본문으로 건너뛰기

2장 타입 스크립트의 타입 시스템

아이템 6 편집기를 사용하여 타입 시스템 탐색하기

  • 타입 스크립트는 언어 서비스 (자동완성, 명세 검사, 검색, 리 팩터링) 을 제공한다.
  • 에디터에서 타입을 확인할 수 있다.
  • 타입 선언 파일을 찾아보는 방법을 터득하자.

아이템 7. 타입이 값들의 집합이라고 생각하기

  • 타입을 값의 집합으로 생각하면 이해하기 편하다.
  • 타입은 엄격한 상속 관계가 아니라 겹쳐지는 집합으로 표현된다.
  • 객체의 추가 속성이 타입 선언에 언급되지 않더라도 그 타입에 속할 수 있다.
  • 타입 연산은 집합 범위에 적용된다.
    • A | B → 합집합
    • A & B → 교집합
  • A는 B를 상속 = A는 B에 할당 가능 = A는 B의 서브타입 = A는 B의 부분집합

아이템 8 타입 공간과 값 공간의 심벌 구분하기

  • 타입 스크립트의 심벌은 타입 공간이나 값 공간 중 한곳에 존재한다.
    • 심벌은 이름이 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있다.
interface Cylinder {
radius: number;
height: number;
}

const Cylinder = (radius: number, height: number) => ({ radius, height });
  • 위 타입 Cylinder와 값 Cylinder는 동시에 존재할 수 있다. 따라서 문맥에 따라 타입인지 값인지 파악할 수 있어야 한다.
  • typeof, this 등 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용될 수 있다.

아이템 9 타입 단언보다는 타입 선언을 사용하기

interface Person {
name: string;
}

const alice: Person = { name: 'Alice' }; // 타입 선언
const bob = { name: 'Bob' } as Person; // 타입 단언
const alice: Person = {
name: 'Alice',
age: 20, // 에러 'age' does not exist in type 'Person'.
};

const bob = {
name: 'Bob',
age: 20,
} as Person; // 에러 X
  • 타입 단언은 강제로 타입을 지정했기 때문에 타입 체커에서 오류를 무시하라고 하는 것과 같다.
    • 타입 선언에서의 안전성 체크가 단언에서는 불가능함
const people = ['alice', 'bob', 'kim'].map((name): Person => ({ name }));
  • 화살표 함수에서의 리턴 값 타입 지 정도 선언문으로 하는 게 좋다.
  • 타입 단언은 타입 체커가 추론한 타입보다 사용자가 판단하는 타입이 더 정확할 때 의미가 있다.
    • ex) DOM element

아이템 10 객체 래퍼 타입 피하기

  • js의 기본 타입(string, number, boolean, null, undefined, symbol, bigint, object)는 객체를 제외하면 메서드를 가지지 않는다.
    • 'abc'.charAt(1) 과 같은 메서드는 'string' 기본형의 메서드가 아니다. 자바스크립트는 기본형을 String 객체로 래핑하고, 메서드를 호출하고, 마지막에 래핑 한 객체를 버린다.
    • 메서드 내의 this는 string 기본형이 아닌 String 객체 레퍼이다.
    • 기본형 string과 객체 래퍼 String은 항상 동일하게 작동하지 않는다
'hello' === 'hello'; // true
new String('hello') === new String('hello'); // false
  • 보통은 래퍼 객체를 직접 생성할 필요가 없다.
  • 기본형 타입은 객체 래퍼 타입에 할당할 수 있지만 반대는 불가능하다.
  • 타입 스크립트는 기본형 타입을 객체 래퍼에 할당하는 선언을 허용하지만, 기본형 타입을 객체 래퍼에 할당하는 구문은 오해하기 쉽고, 굳이 그렇게 할 필요가 없으니 그냥 기본형 타입을 쓰자.

아이템 11 잉여 속성 체크의 한계 인지하기

interface Options {
title: string;
darkMode?: boolean;
}

const options: Options = { title: 'hi', darkmode: false };
/*
'{ title: string; darkmode: boolean; }' 형식은 'Options' 형식에 할당할 수 없습니다.
개체 리터럴은 알려진 속성만 지정할 수 있지만 'Options' 형식에 'darkmode'이(가) 없습니다. 'darkMode'을(를) 쓰려고 했습니까?ts(2322)
*/
  • 타입 스크립트는 해당 타입의 속성이 있는지, 그리고 그 외의 속성은 없는지 확인한다.
  • 위의 darkmode의 경우 darkMode 속성이 옵셔널이기 때문에 options 변수는 구조적 타입 관점에서 오류가 발생하지 않아야 하지만, 잉여 속성 체크 과정에서 오류를 발생시킨다.
    • 잉여 속성 체크를 활용하면 기본적으로 타입 시스템의 구조적 본질을 해치지 않으면서도 객체 리터럴에 알 수 없는 속성을 허용하지 않음으로써 문제를 예방할 수 있다.
const obj = { title: 'hi', darkmode: false };
const options: Options = obj; // 오류 발생 x
  • 잉여 속성 체크는 객체 래터럴에만 적용된다.
    • 위의 코드에선 obj는 임시 변수이기 때문에 잉여 속성 체크가 활성화되지 않는다.
    • 조건에 따라 동작하지 않는 경우가 있다는 한계가 있음.

아이템 12 함수 표현식에 타입 적용하기

// 선언식의 경우
function add(a: number, b: number) {
return a + b;
}
function minus(a: number, b: number) {
return a - b;
}
function mul(a: number, b: number) {
return a * b;
}
function div(a: number, b: number) {
return a / b;
}

// 표현식의 경우
type BinaryFn = (a: number, b: number) => number;
const add2: BinaryFn = (a, b) => a + b;
const minus2: BinaryFn = (a, b) => a - b;
const mul2: BinaryFn = (a, b) => a * b;
const div2: BinaryFn = (a, b) => a / b;
  • 타입 스크립트에서는 함수 선언식 보다 함수 표현식을 사용하는 게 좋다.
    • 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있기 때문
  • 라이브러리에서는 공통 함수 시그니처를 타입으로 제공하기도 한다. (ex. MouseEventHandler)

아이템 13 타입과 인터페이스의 차이점 알기

  • 대부분의 경우 타입을 써도 되고 인터페이스를 써도 된다. 대부분 동일하다.

    • 타입은 인터페이스를 확장할 수 있음.
    type Input = { value: string };
    type Output = { value: string };
    interface VariableMap {
    [name: string]: Input | Output;
    }

    type NamedVariable = (Input | Output) & { name: string };
  • 차이점으로 인터페이스는 유니언 타입 같은 복잡한 타입을 확장하지 못한다.

    • 유니언 타입은 있지만, 유니언 인터페이스라는 개념은 없다.
  • 일반적으로 type이 interface보다 쓰임새가 많다.

    • 유니언이 될 수도 있고, 매핑된 타입 ㄸ/ㅗ는 조건부 타입 같은 고급 기능에 활용되기도 한다.
    type TPair = [number, number];

    interface IPair {
    0: number;
    1: number;
    length: 2;
    }
  • 튜플과 같은 배열 타입도 type을 사용하는 것이 더 간결하다.

    • 인터페이스도 비슷하게 구현할 수는 있지만, concat과 같은 메서드들을 사용할 수 없다.
  • 인터페이스의 강점은 보강이 가능하다.

interface Istate {
name: string;
age: number;
}

interface Istate {
height: number;
}

const kim: Istate = {
name: 'kim',
age: 20,
height: 180,
};
  • 선언 병합이라고 하는데 인터페이스에서만 사용 가능하다.

    • 타입 선언에서 사용자가 채워야 하는 빈틈이 있을 수 있는데, 이 경우 유용하다.
    • 타입 스크립트는 여러 버전의 자바스크립트 표준 라이브러리에서 여러 타입을 모아 병합한다.
      • Array 인터페이스는 lib.es5.d.ts에 정의되어 있지만, tsconfig.json lib 목록에 es2015를 추가한다면 타입 스크립트는 lib.es2015.d.ts에 선언된 인터페이스들을 병합한다.
    • 타입은 기존 타입에 추가적인 보강이 없는 경우에만 사용하면 된다.
    • ps. 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 경우는 잘못된 설계이다.
  • 결론

    • 복잡한 타입이라면 type 사용
    • 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려하자

아이템 14 타입 연산과 제네릭 사용으로 반복 줄이기

  • DRY 원칙 - 같은 코드를 반복하지 마라 - 타입에도 적용해야 한다.

    • 타입에 이름을 붙여서 재사용하기
    • 함수 시그니처를 명명된 타입으로 분리해서 재사용
    • 인터페이스의 부분집합이 공유된다면 공통 필드만 골라서 기반 클래스로 분리
    • 이미 존재하는 타입을 확장하는 경우에 인터섹션(&) 연산자 사용
  • Pick

    • code

      interface State {
      userId: string;
      pageTitle: string;
      recentFiles: string[];
      pageContents: string;
      }

      interface TopNavState {
      userId: string;
      pageTitle: string;
      recentFiles: string[];
      }

      /*
      TopNavState를 확장하여 State를 구성하는 것 보다,
      State의 부분집합으로 TopNavState를 정의하는 것이 바람직 해 보인다. (State가 상위개념이니까)
      State를 인덱싱하여 속성의 타입에서 중복을 제거 할 수 있다.
      */
      type TopNavState2 = {
      userId: State['userId'];
      pageTitle: State['pageTitle'];
      recentFiles: State['recentFiles'];
      };

      // 매핑된 타입 사용
      type TopNavState3 = {
      [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k];
      };

      // 위 매핑된 타입은 Pick과 동일한 패턴
      type TopNavState4 = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
    • Pick은 제너럴 타입이다. 함수를 호출하는 것과 비슷함.

    • type와 key 두 가지 타입을 받아서 결과 타입을 반환한다.

  • keyof

    • 타입을 받아서 속성 타입의 유니언을 반환한다.

    • code

      type Options = {
      width: number;
      height: number;
      color: string;
      };

      type OptionKeys = keyof Options; // 'width' | 'height' | 'color'
      type OptionsUpdate = {
      [k in keyof Options]?: Options[k];
      };
  • typeof

    • 값의 형태에 해당하는 타입을 정의하고 싶을 때 사용한다.
    const INIT_OPTIONS = {
    width: 640,
    height: 480,
    color: 'white',
    };

    /*
    type Options = {
    width: number;
    height: number;
    color: string;
    }
    */
    type Options = typeof INIT_OPTIONS;
  • ReturnType

  • 제네릭 타입은 타입을 위한 함수와 같다.

    • 제네릭 타입에서 매개변수를 제한하기 위한 방법으로 extends 사용

아이템 15 동적 데이터에 인덱스 시그니처 사용하기

type Rocket = { [key: string]: string }; // 인덱스 시그니처
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'v1.0',
}; // 정상
  • 위의 인덱스 시그니처 단점
    • 잘못된 키를 포함해 모든 키를 허용한다.
    • 특정 키가 필요하지 않다.( {} 도 허용 )
    • 키마다 다른 타입을 가질 수 없다.
    • 언어 시스템이 도움이 되지 못한다. ( 자동완성 x)
  • 동적 데이터를 사용하고 싶을 때 쓰자. ( ex. CSV)

아이템 16 number 인덱스 시그니처보다는 Array, 튜 프리, ArrayLike 사용하기

  • key의 타입을 number로 지정하더라도, 런타임에선 string으로 인식된다.
  • number 타입의 키가 필요하다면 배열을 쓰자.
    • 배열의 push, concat 같은 다른 속성이 없어야 한다면 ArrayLike

아이템 17 변경 관련된 오류 방지를 위해 readonly 사용하기

function printTriangles(n: number) {
const nums = [];
for (let i = 0; i < n; i++) {
nums.push(i);
console.log(arraySum(nums));
}
}

function arraySum(arr: readonly number[]) {
let sum = 0,
num;
// ERROR : 'readonly number[]' 형식에 'pop' 속성이 없습니다.ts(2339)
while ((num = arr.pop()) !== undefined) {
sum += num;
}
return sum;
}

printTriangles(5);
  • readonly number[] 특징
    • 배열의 요소를 읽을 수 있지만, 쓸 수는 없다.
    • length를 읽을 수 있지만, 바꿀 수 없다.
    • 배열을 변경하는 pop을 비롯한 다른 메서드를 호출할 수 없다.
  • readonly number[] 보다 number[]의 기능이 많기 때문에 number[]는 서브타입이 된다.
    • 따라서 변경 가능한 배열을 readonly 배열에 할당할 수 있지만, 그 반대는 불가능하다.
  • readonly를 선언하면 다음과 같이 동작한다
    • 타입 스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크함
    • 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게 된다.
    • 호출하는 쪽에서 함수에 readonly 배열을 매개변수로 넣을 수 있다.

아이템 18 매핑된 타입을 사용하여 값을 동기화하기