221023 TIL | 이펙티브 타입스크립트_#4 타입 단언보다 타입 사용하기 등

아이템9 ~ 아이템 11까지 정리

  • 아이템 9 : 타입 단언보다 타입 선언 사용하기
  • 아이템10 : 객체 래퍼 타입 피하기
  • 아이템11 : 잉여 속성 체크의 한계 인지하기

타입 단언보다 타입 선언 사용하기

  • 자바스크립트에서 타입을 선언하는 방법은 두 가지가 있다.
    1. 타입 단언
    2. 타입 선언
interface Person { name: string };

// 타입 단언 방식
const alice: Person = { name: 'Alice' };  // Type is Person
// 타입 선언 방식
const bob = { name: 'Bob' } as Person;  // Type is Person
  • 타입 단언보다 타입 선언을 사용하는 것이 좋다.
    • 타입 단언은 안정성 체크를 하지 않고, 강제로 타입을 지정하기 대문에 타입 체커에서 오류를 무시한다.
  • 유형에 필요한 속성을 지정하지 않았을 때, 속성을 추가할 때도 마찬가지이다.
// name 속성을 지정하지 않았을 때
interface Person { name: string };
const alice: Person = {};
   // ~~~~~ 'Person' 타입에 'name' 속성이 필요한데 '{}' 유형에 없다. 
const bob = {} as Person;  // 에러 없음

// 속성을 추가할 때
const alice: Person = {
  name: 'Alice',
  occupation: 'TypeScript developer'
// ~~~~~~~~~~ 'Person' 형식에 'occupation'가 없다.
//						개체 리터럴은 알려진 속성만 지정할 수 있다.
};
const bob = {
  name: 'Bob',
  occupation: 'JavaScript developer'
} as Person;  // 에러 없음
// 해당 단언문은 타입 단언의 원래 문법이다. 
// {} as Person과 똑같이 작동한다.
const bob = <Person>{};
// 쓰지 맙시다.

화살표 함수 안에서 타입 선언문

  • 화살표 함수를 사용할 때 타입 단언문을 이용하면 정말 편하겠지만, 위와 마찬가지로 오류가 발생할 수 있다.
  • 타입 선언문 예시
interface Person { name: string };

// 1. 변수를 사용하는 방법
const people = ['alice', 'bob', 'jan'].map(name => {
  const person: Person = {name};
  return person
}); // Type is Person[]

// 2. 화살표 함수를 사용하는 방법
// 함수 호출 체이닝이 연속되는 경우 체이닝 시작부터 명명된 타입을 가져야 한다.
// return 받을 타입이 Person[]임을 반드시 명시한다.
const people: Person[] = ['alice', 'bob', 'jan'].map(
	// (name): Person은 name의 타입이 없고, 반환 타입이 Person이라는 것을 명시한다.
  (name): Person => ({name})
); // Type is Person[]

타입 단언이 꼭 필요한 경우

  • 타입 체커가 추론한 타입보다 개발자가 판단하는 타입이 더 정확할 때가 있다.
    • ex ) DOM 엘리먼트에 대해 타입 추론을 할 때
    • 타입스크립트는 DOM에 접근할 수 없다.
// tsConfig: {"strictNullChecks":false}

document.querySelector('#myButton').addEventListener('click', e => {
  e.currentTarget // 타입은 EventTarget
  const button = e.currentTarget as HTMLButtonElement;
  button // 타입은 HTMLButtonElement
});

Null 단언문

  • 단언문은 타입 체커가 알지 못하기 때문에 들어오는 값이 null이 아닌 것이 확실할 때 사용한다.
  • null일수도 있다면 조건문을 사용해야 한다.
const elNull = document.getElementById('foo');  // Type is HTMLElement | null
const el = document.getElementById('foo')!; // Type is HTMLElement

객체 래퍼 타입 피하기

> primitive.charAt(3)
"m'
  • 자바스크립트에서 코드를 실행했을 때 charAt이 string 타입의 메서드인 것처럼 보이지만, String 객체 타입의 메서드가 호출된 것이다.
    • 키워드 : 래퍼 객체
  • 타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링한다.
    • string과 String
    • number와 Number
    • boolean과 Boolean
    • symbol과 Symbol
    • bigInt와 BigInt

잉여 속성 체크의 한계 인지하기

예제 1)

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}
const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,
//                    and 'elephant' does not exist in type 'Room'
};
  • 해당 코드를 구조적 타이핑 관점으로 생각해보면 오류가 발생하지 않아야 한다.

예제 2)

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}
// 임시 변수
const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
};
const r: Room = obj;  // OK
  • 임시 변수 obj 객체는 Room 타입에 할당이 가능하다.
  • 타입 체커가 추론한 obj 타입은 Room 타입의 부분 집합을 포함하므로 통과된다.
  • 두 예제의 차이점은 예제 1에서는 잉여 속성 체크 과정이 수행되었고 예제 2에서는 과정이 수행되지 않았다는 것이다.

타입스크립트는 런타임에 예외를 던지는 코드에 오류를 표시하는 것뿐 아니라, 의도와 다르게 작성한 코드까지 찾으려 한다는 것을 잊지 말자.

잉여 속성 체크란?

  • 구조적 타입 시스템에서 발생할 수 있는 오류를 잡을 수 있도록 하는 과정이다.
  • 타입 시스템의 구조적 본질을 해치지 않고 객체 리터럴에 알 수 없는 속성을 허용하지 않는 기능을 담당한다.

잉여 속성 체크가 적용되지 않는 경우

  • 객체 리터럴이 아닌 경우
  • 타입 단언문을 사용할 때

만약 잉여 속성 체크를 사용하고 싶지 않다면?

  • 앞선 예제처럼 임시 변수를 사용하면 잉여 속성 체크를 건너뛸 수 있다.
  • 혹은 인덱스 시그니처를 이용하여 타입스크립트가 추가적인 속성을 예상하도록 할 수 있다.
    • 이런 방법이 데이터 모델링에 적절한지 여부는 추후(아이템 15)에 다룬다.
interface Options {
  darkMode?: boolean;
  [otherOptions: string]: unknown;
}
const o: Options = { darkmode: true };  // OK

출처

  • 이펙티브 타입스크립트