Ayden's journal

이펙티브 타입스크립트

이게 독후감인지는 모르겠지만, 아무튼 책을 읽고 남기는 내용이니까... 

 

Chap 1. 타입스크립트 알아보기

타입스크립트 컴파일러(혹은 트랜스파일러)는 크게 두 가지 작업을 수행한다. 하나는 TS로 작성한 파일을 JS로 트랜스파일 하는 것이고, 다른 하나는 코드에 타입 오류가 있는지를 체크하는 것이다. tsconfig.json를 통해 아래와 같은 몇 가지 유용한 컴파일러 옵션을 적용할 수 있다.

  • noImplicitAny : 특정 변수가 암시적으로 any 타입을 갖지 않도록 방지해줌
  • strictNullCheck : 모든 타입에서 null과 undefined를 허용할 것인지 결정
  • noEmitOnError : 타입 오류가 있을 시 컴파일 자체를 종료해버림

 

구조적 타이핑

자바스크립트는 기본적으로 덕 타이핑(duck typing) 기반이다. 이는 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 그 객체를 해당 타입에 속하는 것으로 간주하는 방식을 말한다.

type Coord2D = {
  x: number;
  y: number;
}

type Coord3D = {
  x: number;
  y: number;
  z: number;
}

const dash = (coord: Coord2D): Coord2D => {
  return {
    x: coordinate.x + 2,
    y: coordinate.y + 2
  }
}

const coord3D: Coord3D = {x: 1, y: 1, z: 1}
dash(coord3D)

위의 예시 코드에서 함수 dash에는 Coord2D 타입에 해당하는 객체를 매개변수로 받겠다고 선언해두었다. 그런데 이 dash 함수에 Coord3D 타입에 해당하는 객체를 인자로 제공해도 함수는 문제 없이 실행된다. 이는 Coord3D 타입의 구조가 Coord2D와 호환되기 때문이다. 이를 구조적 타이핑(structural typing)이라 한다.

호출에 사용되는 매개변수가 타입에 선언된 속성만을 가질 거라 생각하기 쉽다. 하지만 좋든 싫든 타입은 열려있다. 이러한 특성 때문에 때로는 당혹스러운 결과가 발생하기도 한다.

function calculateLengthL1(v: Coord3D) {
  let length = 0;
  for (const axis of Object.keys(v)) {
    const coord = v[axis];
               // ~~~~~~~ 'string'은 'Vector3D'의 인덱스로 사용할 수 없기에
               //         엘리먼트는 암시적으로 'any' 타입입니다.
    length += Math.abs(coord);
  }
  return length;
}

 

반복문에서 선언한 변수 axis는 Coord3D 타입의 keys 값으로 이루어진 배열의 인덱스를 하나씩 받기 때문에 차례대로 "x", "y", "z" 가 되고, 다른 값이 되지는 않을 것만 같다. 그러나 실제로는 아래와 같은 일이 발생할 수 있다.

const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D);  // OK, returns NaN

구조적 타이핑이 야기하는 이와 같은 문제는 처리하기가 몹시 까다로운데, 이런 경우는 반복문보다는 모든 속성을 각각 더하는 구현이 더 낫다.

function calculateLengthL1(v: Vector3D) {
  return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}

 

 

테스트 코드 작성에서의 구조적 타이핑

아래와 같이 데이터베이스에 쿼리하고 그 결과를 처리하는 함수를 가정해보자. 이 함수는 매개변수로 PostgresDB 타입의 값을 받게 되어있다. 그런데 이와 같이 너무 큰 타입을 매개변수로 받아버리면, 추후 테스트를 진행하기 위해 함수를 호출할 때 PostgresDB 타입의 속성을 모두 만족하는 인자를 넘겨주어야만 하게 된다.

interface Author {
  first: string;
  last: string;
}
function getAuthors(database: PostgresDB): Author[] {
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

이런 경우에는 구조적 타이핑을 활용하여 구체적인 인터페이스를 정의하는 것이 더 나은 방법이다.

interface DB {
  runQuery: (sql: string) => any[];
}

function getAuthors(database: DB): Author[] {
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

test('getAuthors', () => {
  const authors = getAuthors({
    runQuery(sql: string) {
      return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
    }
  });
  expect(authors).toEqual([
    {first: 'Toni', last: 'Morrison'},
    {first: 'Maya', last: 'Angelou'}
  ]);

 

 

Chap 2. 타입스크립트의 타입 시스템

'할당 가능한 값들의 집합'으로서의 타입

다양한 타입스크립트 오류에서 '할당'이라는 문구를 볼 수 있다. 이는 집합의 관점에서 타입 체커의 역할이 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이기 때문이다.

let a: "A" = "A";
let b: string = "B";

a = b // 'string' 형식은 '"A"' 형식에 할당할 수 없습니다.

변수 a는 스트링 리터럴 타입으로서, "A" 외에는 그 어떤 값도 할당할 수 없는 변수이다. string 타입은 "A" 타입의 부분 집합이 아니다. 따라서 변수 a에 변수 b를 할당할 수 없는 것이다. 기억하기 쉽게 이를 나만의 방법으로 설명한다면 "큰 타입을 작은 타입에 억지로 쑤셔 넣으려다간 변수가 찢어져버렷"... 

 

객체 리터럴 타입의 유니언 타입과 인터섹션 타입에 대해 이해하는 데 시간이 꽤 들었다. 이 각각의 타입을 합집합과 교집합으로 설명하는 것은 나에게는 큰 도움이 되지 않았다. 나는 이것들을 할당 가능한 값들의 집합 그 자체로 받아들이려 노력했다. 가령 아래와 같은 타입이 있다고 해보자.

type Person = {
  name: string;
  canStand: boolean;
}

type Wolf = {
  name: string;
  canHowl: boolean;
}

type WereWolf = Person & Wolf;
type PersonWolf = Person | Wolf

WereWolf 타입은 Person 타입과 Wolf 타입이 가지고 있는 프로퍼티 속성을 모두 합친 '단 하나의 객체 리터럴 타입'이다. 따라서 이 타입은 name, canStand, canHowl을 모두 가지고 있다.

반면에 PersonWolf 타입은 Person 타입과 Wolf 타입 둘 다가 될 수 있는 가능성을 가진 '여러 객체 리터럴 타입의 합'이다. 이 때문에 객체 리터럴의 유니언 타입을 매개변수로 쓰는 함수는 그 내부에서 '타입 좁히기'를 하지 않는 이상 아래와 같은 에러를 맞닥트리게 된다.

 

readonly

어떤 타입은 변수에 할당된 값을 수정할 수 없도록 readonly하게 만들 수 있다. const로 선언한 변수라 해도 배열이나 객체는 그 내부의 프로퍼티 값을 변경할 수 있는데, readonly 키워드를 사용하면 배열과 객체도 진정한 의미의 상수로 사용할 수 있게 된다. 다만 readonly는 깊은 비교를 하지 않고 얕은 비교만 하기 때문에 겹겹이 중첩된 객체의 경우에는 값 변경을 막을 수 없다.

 

extends

타입스크립트에서 extends 키워드는 크게 세 가지 방식으로 사용된다.

  • interface A extends B ➠ B 타입을 A 타입에 상속한다는 의미. 따라서 A 타입은 B 타입의 부분 집합이 된다.
  • <K extends U> ➠ 제네릭 타입 K가 U를 상속받고 있다는 의미. K 타입의 범위를 제한하는 '한정자'로 사용된다.
  • T extends U ? string : number ➠ 조건부 타입에서 T 타입이 U 타입의 부분 집합인지 아닌지를 확인한다.

 

타입 단언

일반적으로는 타입 선언을 사용해야 하지만, 몇몇 특수한 경우에는 타입 단언을 사용하기도 한다. DOM element와 event 등 TS의 타입 추론보다 개발자가 타입을 더 잘 아는 경우, Null이 아님을 확신할 때 이를 단언하는 ! 접미사, 그리고 타입을 극한으로 좁히는 as Const 단언 등이 그것이다.

 

잉여 속성 체크

타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지'를 확인한다. 아래의 예시를 살펴보면 타입 에러가 발생한 것을 확인할 수 있는데, 이는 구조적 타이핑 시스템에서 발생할 수 있는 중요한 종류의 오류를 잡을 수 있도록 '잉여 속성 체크'라는 과정이 수행되었기 때문이다.

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}

const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
// ~~~~~~~~~~~~~~~~~~ 개체 리터럴은 알려진 속성만 지정할 수 있으며
//                    'elephant' 형식에 'Room'이(가) 없습니다
};

그러나 임시 변수 obj를 도입하면 맥락상으로는 다르지 않음에도 불구하고 '잉여 속성 체크'가 수행되지 않는다. 이는 잉여 속성 체크 과정이 객체 리터럴 할당 과정에서만 수행되기 때문이다.

const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
};

const r: Room = obj;  // OK

잉여 속성 체크 과정을 우회하고자 한다면 인덱스 시그니처를 사용해 타입스크립트로 하여금 추가적인 속성을 예상할 수 있도록 해야 한다.

 

타입과 인터페이스

일반적으로 interface 키워드를 통해 선언할 수 있는 타입이라면 대부분 type 키워드를 통해서도 선언할 수 있다. 유니언 타입은 있지만 유니언 인터페이스는 존재하지 않는다. 매핑된 타입이나 조건부 타입과 같은 고급 기능도 인터페이스 키워드로는 구현할 수 없다.

그러나 인터페이스에는 타입에 없는 몇 가지 기능이 있다. 그 중 하나는 보강(augment)이 가능하다는 것이다. 아래와 같은 방식으로 속성을 확장하는 것을 선언 병합(declaration merging)이라고 한다.

interface State {
  name: string;
  capital: string;
}

interface State {
  population: number;
}

const wyoming: State = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000
};  // 정상

 

타입 연산을 활용한 반복 줄이기

타입 연산이란 기존의 타입으로부터 정해진 연산 과정을 거쳐 새로운 타입을 도출해내는 일련의 과정이라고 생각한다. 타입 인덱싱, 타입 매핑, 유틸리티 타입, 제네릭 타입 등을 활용하여 타입 연산을 수행한다. 자주 사용되는 유틸리티 타입들은 아래와 같다.

  • Partial<TYPE>, Required<TYPE>, Readonly<TYPE> : TYPE의 모든 속성을 각각 선택적, 필수, 읽기 전용으로 변경한 새로운 타입을 반환
  • Omit<TYPE, KEY>, Pick<TYPE, KEY> : TYPE에서 KEY로 속성을 포함하거나 제외한 새로운 타입을 반환
  • Record<KEY, TYPE> : KEY를 속성으로, TYPE를 그 속성값의 타입으로 지정하는 새로운 타입을 반환
  • Parameters<TYPE>, ReturnType<TYPE> : 함수 타입을 받으면 각각 매개변수의 타입과 리턴하는 값의 타입으로 된 새로운 타입을 반환
// 타입 인덱싱
type CanBark = WereWolf["canHowl"] // boolean

// 유니언 타입의 인덱싱
type Dog = { tag: "Dog" }
type Cat = { tag: "Cat" }
type CreatureTag = (Dog|Cat)["tag"] // "Dog" | "Cat"

// 타입 매핑
type PartialWereWolf= {
  [k in keyof WereWolf]?: WereWolf[k]
}

 

@TODO 제네릭 관련 서술

 

@TODO 타입 연산을 활용해 반복을 줄이기 위해서는 작은 타입들을 만들고 이를 extends 하며 큰 타입들을 만들 수도 있고, 반대로 큰 타입을 만들고 Pick이나 Omit을 활용해 작은 타입으로 나눠낼 수도 있겠다. 각각 언제 써야할지가 다를 듯.

 

 

Chap 3. 타입 추론

어떤 언어들은 매개변수의 최종 사용처까지 참고하여 타입을 추론하지만, 타입스크립트는 그러지 않는다. 타입스크립트에서 변수의 타입은 일반적으로 처음 등장할 때 결정된다. 따라서 컴파일러에 의해 타입 추론이 가능할지라도 구현상의 오류가 함수를 호출한 곳까지 영향을 미치지 않도록 하기 위해 함수의 반환 타입을 명시해두는 게 좋다.

 

타입 넓히기와 타입 좁히기

런타임에 변수는 유일한 값을 가지지만, 타입스크립트가 코드를 체크할 때는 할당 가능한 값들의 집합인 타입을 가진다. 상수를 사용해 변수를 초기화할 때, 타입 체커는 타입을 결정해야 한다. 이 말은 '지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추'하게 된다는 것인데, 이를 타입스크립트에서는 '타입 넓히기'라고 부른다.

let a = "ace" // 타입 추론의 결과는 "ace"가 아니라 string
let b = ["20", 20] // 타입 추론의 결과는 [string, number]가 아니라 (string|number)[]

타입 넓히기를 억제하기 위해서는 몇 가지 방법을 사용할 수 있는데, '명시적 타입을 부여'하거나 변수를 const로 선언할 수도 있고, as const 단언을 통해 최대한 좁은 타입으로 추론하게 만들 수도 있다.

 

타입 좁히기는 타입 체커가 넓은 타입으로부터 좁은 타입을 유추할 수 있도록 하는 테크닉이다. '할당 가능한 값들의 집합'으로서의 타입 마지막 부분에서 나는 "객체 리터럴의 유니언 타입을 매개변수로 쓰는 함수는 그 내부에서 '타입 좁히기'를 하지 않는 이상 아래와 같은 에러를 맞닥트리게 된다."고 이야기했다.

const personWolf = (creature: PersonWolf) => {
  return {
    name: creature.name,
    canHowl: creature.canHowl,
    canStand: creature.canStand,
  };
};

위의 코드가 문제가 되는 이유는 간단하다. PersonWolf 타입은 Person 타입과 Wolf 타입의 유니언 타입이기 때문에, 함수 내에서 이 타입은 고정되어있지 않고 마치 슈뢰딩거의 고양이처럼 '확률'로만 존재하게 된다. 따라서 우리는 ─ 양자역학의 거장들이 그러했던 것처럼 ─ 관측을 통해 이 확률을 고정시켜주어야 한다.

const personWolf = (creature: PersonWolf) => {
  // 변수에 canHowl 프로퍼티가 존재한다면,
  // 타입은 Wolf로 고정된다
  if ("canHowl" in creature) {
    ...
  } else {
    ...
  }
};

조건문을 통해 타입이 잘 좁혀진 것을 확인할 수 있다. 이렇게 조건문을 사용해 타입 좁히기를 할 때에는 특정 타입만이 가지고 있는 프로퍼티를 사용하는 방법 외에도 태그된 유니언(tagged union) 또는 구별된 유니언(discriminated union) 이라 불리는 패턴을 사용할 수도 있다. 이는 타입에 특정 태그를 달아두어 해당 태그를 통해 타입을 추론하게 하는 것이다.

type Person = {
  tag: "person"
  name: string;
}

type Wolf = {
  tag: "wolf"
  name: string;
}

type PersonWolf = Person | Wolf

const isWolf = (thing: PersonWolf) => {
  if (thing.tag == "person") {
    return false
  }
  
  return true;
}

 

사용자 정의 타입 가드

타입을 좁히는 또 하나의 방법은 '사용자 정의 타입 가드(custom type guard)'를 사용하는 것이다. 이는 위에서 살펴보았던 조건문을 사용한 타입 좁히기를 함수로 분리하여 재사용성을 높이는 데 도움을 준다.

const isPerson = (thing: PersonWolf): thing is Person => {
  return "canStand" in thing
}

is는 타입스크립트의 특수한 문법으로서 타입 체커로 하여금 변수의 타입을 좁힐 수 있게 해준다. 위에 작성한 thing is Person은 '만약 함수의 return이 true라면 변수 thing의 타입은 Person'이라는 의미이다.

const spliter = (thing: PersonWolf): Person | Wolf => {
  if (isPerson(thing)) {
    return {
      name: thing.name,
      canStand: thing.canStand,
    } as Person;
  } else {
    return {
      name: thing.name,
      canHowl: thing.canHowl,
    } as Wolf;
  }
};

만약 위의 코드에서 타입 단언하는 게 싫다면 아래처럼 각각 변수를 사용해 처리해줄 수 있다.

const spliter = (thing: PersonWolf): Person | Wolf => {
  if (isPerson(thing)) {
    const person = {
      name: thing.name,
      canStand: thing.canStand,
    };

    return person;
  } else {
    const wolf = {
      name: thing.name,
      canHowl: thing.canHowl,
    };

    return wolf;
  }
};

 

 

Chap 4. 타입 설계

타입을 설계함에 있어 문서나 주석에 타입에 대한 정보를 작성하는 것은 권장되지 않는다. 최악의 경우 주석의 설명과 타입의 내용이 모순되는 경우가 발생할 수 있다. 따라서 타입 그 자체가 자신의 정보를 드러낼 수 있도록 하는 것이 가장 좋다. TSdoc을 활용해 타입을 설명하고 싶을 때에도 해당 타입 정보를 명시하지 말아야 한다.

 

한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되어서는 안 된다. 명확한 값이 null과 섞이게 하기보다는, 반환 타입을 큰 객체로 만들고 반환 타입 전체가 null이거나 null이 아니게 만들어야 한다. 그리고는 호출할 때 ! 단언을 사용하여 외부로 null이 새어나오지 않게 해야 한다. 앞서 소개한 strictNullChecks 옵션이 도움이 될 것이다.

 

가능하다면 string 타입보다 더 구체적인 타입을 사용하는 것이 좋다. keyof 를 활용하면 타입을 구체적으로 정의하는 데 도움이 된다. 하지만 부정확한 타입보다는 언제나 미완성 타입을 사용해야 한다. 타입이 없는 것보다 타입이 잘못된 게 더 다.

 

유니온으로 된 인터페이스와 인터페이스로 된 유니온 사이에서 타입을 어떻게 설계할지 고민해보는 건 늘 좋은 일이다. 가령 아래와 같은 경우를 생각해보자. 아래의 Layer 타입은 유니온으로 된 인터페이스이다. 만약 타입이 'fill'인데 layout이 LineLayout이고 paint가 PointPaint인 변수를 허용해야 한다면 이렇게 유니온으로 된 인터페이스를 사용할 수도 있을 것이다.

type Layer = {
  type: 'fill' | 'line' | 'point';
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}

하지만 이런 경우는 타입이 'fill'일때 layout과 paint 속성이 FillLayout과 FillPaint인 편이 더 자연스러울 것이다. 따라서 아래와 같이 인터페이스로 된 유니온 타입으로 Layer 타입을 설계하는 것이 더 적합하다.

interface FillLayer {
  type: 'fill';
  layout: FillLayout;
  paint: FillPaint;
}
interface LineLayer {
  type: 'line';
  layout: LineLayout;
  paint: LinePaint;
}
interface PointLayer {
  type: 'paint';
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

 

 

Chap 5. any 다루기

any를 사용하지 않을 수 있다면 좋겠지만, 반드시 사용해야 하는 순간들이 있을 수 있다. 그럴 때는 가능한 좁은 범위에서만 사용할 수 있도록 해야한다. 아래의 코드를 보자.

interface Foo { foo: string; }
interface Bar { bar: string; }
declare function expressionReturningFoo(): Foo;
function processBar(b: Bar) { /* ... */ }

function f1() {
  const x: any = expressionReturningFoo();  // Don't do this
  processBar(x);
}

function f2() {
  const x = expressionReturningFoo();
  processBar(x as any);  // Prefer this
}

x: any보다 x as any가 더 권장되는데, 이는 any 타입이 processBar 함수의 매개변수에서만 사용되었기 때문에 다른 코드에는 영향을 미치지 않기 때문이다. 만약 위의 함수들이 x를 반환하게 된다면 f1 함수의 리턴 타입은 any가 되고, f2 함수의 리턴 타입은 Foo가 될 것이다. 함수의 리턴 타입이 any인 경우 타입 안정성이 나빠지기에 절대 any 타입을 리턴하는 일이 없도록 하자.

function f1() {
  const x = expressionReturningFoo();
  // @ts-ignore
  processBar(x);
  return x;
}

@ts-ignore을 사용하면 바로 다음 줄의 타입 에러가 무시되지만, 근본적인 문제를 해결한 것은 아니기에 다른 곳에서 더 큰 문제가 발생할 수 있다. 근본적인 원인을 찾아 대처하는 것이 바람직하다.

 

noImplicitAny 옵션을 사용하면 암시적 any를 제거해나가는 데 도움이 되지만, 외부 라이브러리 등에서 유입되는 any 혹은 명시적으로 선언한 any를 막을 수는 없다. type-coverage 패키지와 --detail 플래그를 활용해 any가 존재하는 곳을 관리해주는 것이 좋다.

 

구체적인 any

any 타입은 타입스크립트로 가능한 모든 타입을 포함하는 치트키 같은 타입이다. 이는 일반적인 상황이라면 any보다 더 구체적으로 표현할 수 있는 타입이 존재한다는 의미가 되기도 한다. 따라서 any를 사용하고 싶은 순간에 더 구체적인 타입을 고민하여 타입 안전성을 높이도록 해야 한다.

가령 함수의 매개변수가 객체이긴 하지만 값을 알 수 없다면 {[k: string]: any} 와 같은 방식을 사용할 수 있을 것이다.

function getLengthBad(array: any) {  // 비추
  return array.length;
}

function getLength(array: any[]) {
  return array.length;
}

getLengthBad(/123/);  // 에러 없이 undefined가 리턴됨
getLength(/123/);
       // ~~~~~ 'RegExp' 형식의 인수는
       //       'any[]' 형식의 매개 변수에 할당될 수 없습니다.

 

@TODO any와 unknown에 대한 아이템 42에 대한 내용 추가

 

any의 진화

any 타입의 진화는 타입 좁히기와는 다른 개념이다. noImplicitAny 옵션이 설정된 상태로 변수의 타입이 암시적으로 any인 상황에서 어떠한 값을 할당할 때에만 발생하는 현상이다. 따라서 명시적으로 any 타입을 할당한 경우에는 발생하지 않는다.

const result = [];  // Type is any[]
result.push('a');
result  // Type is string[]
result.push(1);
result  // Type is (string | number)[]

위의 예시를 통해 array에 push 하는 값의 타입에 따라 변수 result의 타입이 any[]에서 string[], (string | number)[] 타입으로 진화해나가는 것을 알 수 있다.

 

 

Chap 7. 코드를 작성하고 실행하기

객체를 순회하는 노하우

아래의 코드는 정상적으로 실행되지만 타입 에러가 발생한다. k의 타입은 스트링으로 추론되지만 obj 타입에는 정해진 문자열만 존재하기 때문에 불일치가 발생하여 에러가 발생하는 것이다. 따라서 k의 타입을 더욱 구체적으로 명시해주면 오류는 사라진다.

const obj = {
  one: "uno",
  two: "dos",
  three: "tres",
};

const makeArr = (object: typeof obj) => {
  let arr = [];

  for (let k in object) {
    const v = object[k];
           // ~~~~~~~~~ object에 인덱스 시그니처가 없기 때문에
           //           엘리먼트는 암시적으로 'any' 타입입니다.
    arr.push(v);
  }

  return arr;
};
const makeArr = (object: typeof obj) => {
  let arr = [];
  let k: keyof typeof obj; // Type is "one" | "two" | "three"

  for (k in object) {
    const v = object[k]; // OK
    arr.push(v);
  }

  return arr;
};

 

DOM 계층 구조의 이해

HTMLElement는 HTMLxxxElement보다 넓은 타입이다. 가령 여러 태그에 공통적으로 사용되는 className이나 style, id 등의 속성은 HTMLElement 타입에 선언된 속성이지만, value는 HTMLInputElement에만 있는 속성이다. 마찬가지로 Event는 xxxEvent보다 넓은 타입이라, shiftKey 속성은 MouseEvent 타입에 선언된 속성이다.

// 정확한 타입을 추론해내는 메소드
document.getElementsByTagName('p')[0];  // HTMLParagraphElement
document.createElement('button');  // HTMLButtonElement
document.querySelector('div');  // HTMLDivElement

// 정확한 타입을 추론하지 못하는 메소드
document.getElementById('my-div');  // HTMLElement

일반적으로 단언을 지양해야 하기는 하지만, DOM에 대해서는 타입스크립트가 정확한 타입을 추론하지 못하는 경우가 있으니, 예외적으로 as HTMLDivElement과 같이 DOM 단언을 통해 알맞은 타입을 제공해주는 것이 좋다.

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기