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

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

생성일
Oct 9, 2021 01:50 AM
태그
ts

아이템 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 매핑된 타입을 사용하여 값을 동기화하기

 
 
 
 
 

Loading Comments...