이펙티브 타입스크립트 - 2장 (1)

Sohee Park·2023년 1월 7일
5
post-thumbnail

2장에서는 타입 시스템의 기초부터 살펴본다.

ITEM 06 편집기를 사용하여 타입 시스템 탐색하기

타입스크립트를 설치하면 다음 두가지를 실행할 수 있다.

  • 타입스크립트 컴파일러(tsc)
  • 단독으로 실행할 수 있는 타입스크립트 서버(tsserver)

보통 타입스크립트 컴파일러를 실행하는 것이 주된 목적이지만, 타입스크립트 서버 또한 '언어 서비스'를 제공한다는 점에서 중요하다. 언어서비스에는 코드 자동 완성, 명세(사양, specification) 검사, 검색, 리팩터링이 포함된다.

타입스크립트가 값을 추론할 때 기대한 값과 추론한 타입이 다를 수 있는데, 이럴 땐 직접 타입을 명시한다.
이외에도 편집기 상의 타입 오류를 살펴보는 것도 타입 시스템의 성향을 파악하는데 좋은 방법이다. 아래의 코드를 예시로 살펴본다.

function getElement(elOrId: string|HTMLElement|null): HTMLElement {
  if (typeof elOrId === 'object') {
    return elOrId;
 // ~~~~~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'
  } else if (elOrId === null) {
    return document.body;
  } else {
    const el = document.getElementById(elOrId);
    return el;
 // ~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'
  }
}

자바스크립트에서 typeof nullobject이므로, elOrId는 여전히 분기문 내에서 null일 가능성이 있다. 그러므로 의도한 대로 분기처리를 하려면 처음에 null 체크를 추가해서 바로 잡는다.

언어 서비스는 라이브러리와 라이브러리의 타입 선언을 탐색할 때도 도움이 된다. 코드 내에서 fetch함수를 호출 하고 이 함수를 더 알아보길 원한다고 할 때, 편집기는 Go to Definition(정의로 이동) 옵션을 제공한다.
이 옵션을 선택하면 타입스크립트에 포함되어 있는 DOM 타입 선언인 lib.dom.d.ts로 이동한다. 이 파일에서 더 많은 타입을 탐색하다 보면, 어떻게 라이브러리가 모델링되었는지, 어떻게 오류를 찾아낼지 살펴볼 수 있다.

ITEM 07 타입이 값들의 집합이라고 생각하기

가장 작은 집합은 아무 값도 포함하지 않는 공집합이며, 타입스크립트에서는 never타입이 여기에 해당한다. never타입으로 선언된 변수의 범위는 공집합이기 때문에 아무런 값도 할당할 수 없다.

다음으로 작은 집합은 한가지 값만 포함하는 타입이다. 이들은 타입스크립트에서 유닛(unit)타입이라고 불리는 리터럴(literal) 타입이다.

type A = 'A';
type B = 'B';
type Twelve = 12;

타입스크립트의 타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용된다.

interface Identified {
  id: string;
}
interface Person {
  name: string;
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type PersonSpan = Person & Lifespan;

const ps: PersonSpan = {
  name: 'Alan Turing',
  birth: new Date('1912/06/23'),
  death: new Date('1954/06/07'),
};  // OK

& 연산자는 두 타입의 인터섹션(intersection, 교집합)을 계산한다. 값의 집합으로 생각해보면, 인터섹션 타입의 값은 각 타입 내 속성을 모두 포함하는 것이 일반적인 규칙이다.
규칙이 속성에 대한 인터섹션에 관해서는 맞지만, 유니온에서는 그렇지 않다.

interface Identified {
  id: string;
}
interface Person {
  name: string;
}
interface Lifespan {
  birth: Date;
  death?: Date;
}
type PersonSpan = Person & Lifespan;
type K = keyof (Person | Lifespan);  // Type is never

앞의 유니온 타입에 속하는 것은 어떠한 키도 없기 때문에, 유니온에 대한 keyof는 공집합(never)이어야 한다.
'A는 B를 상속(extends)', 'A는 B에 할당 가능', 'A는 B의 서브타입'은 'A는 B의 부분 집합'과 같은 의미다.

ITEM 08 타입 공간과 값 공간의 심벌 구분하기

타입스크립트의 심벌(symbol)은 타입 공간이나 값 공간 중의 한 곳에 존재한다.

interface Cylinder {
  radius: number;
  height: number;
}

const Cylinder = (radius: number, height: number) => ({radius, height});

심벌은 이름이 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있기 때문에 혼란스러울 수 있다. 상황에 따라서 Cylinder는 타입으로 쓰일 수 있고, 값으로도 쓰일 수 있어서 오류를 야기하기도 한다.

function calculateVolume(shape: unknown) {
  if (shape instanceof Cylinder) {
    shape.radius
       // ~~~~~~ Property 'radius' does not exist on type '{}'
  }
}

instanceof는 자바스크립트의 런타임 연산자이고, 그러므로 타입이 아닌 값에 대해서 연산을 하기 때문에, instanceof Cylinder는 타입이 아니라 함수를 참조해서 에러가 발생한다.

두 공간에 대한 개념을 잡으려면 타입스크립트 플레이그라운드를 활용하는 것이 좋다. 타입스크립트 소스로부터 변환된 자바스크립트 결과물을 보여준다. 컴파일 과정에서 타입 정보는 제거되기 때문에, 심벌이 사라진다면 그것은 타입에 해당될 것이다.

interface Person {
  first: string;
  last: string;
}
const p: Person = { first: 'Jane', last: 'Jacobs' };
//    -           --------------------------------- Values
//       ------ Type
function email(p: Person, subject: string, body: string): Response {
  //     ----- -          -------          ----  Values
  //              ------           ------        ------   -------- Types
  // COMPRESS
  return new Response();
  // END
}

class Cylinder {
  radius=1;
  height=1;
}

function calculateVolume(shape: unknown) {
  if (shape instanceof Cylinder) {
    shape  // OK, type is Cylinder
    shape.radius  // OK, type is number
  }
}
type T1 = typeof p;  // Type is Person
type T2 = typeof email;
    // Type is (p: Person, subject: string, body: string) => Response

const v1 = typeof p;  // Value is "object"
const v2 = typeof email;  // Value is "function"

타입의 관점에서 typeof는 값을 읽어서 타입스크립트 타입을 반환하지만, 값의 관점에서 typeof는 자바스크립트 런타임의 typeof연산자가 된다.

class키워드는 값과 타입 두가지로 모두 사용되는데, 클래스에 대한 typeof는 상황에 따라 다르게 동작한다.

const v = typeof Cylinder;  // Value is "function"
type T = typeof Cylinder;  // Type is typeof Cylinder
declare let fn: T;
const c = new fn();  // Type is Cylinder

속성접근자인 []는 타입으로 쓰일 때에도 동일하게 동작한다. 그러나 obj['field']obj.field는 값이 동일하더라도 타입은 다를 수 있다. 따라서 타입의 속성을 얻을 때에는 반드시 첫번째 방법(obj['field'])를 사용해야 한다.

const first: Person['first'] = p['first'];  // Or p.first
   // -----                    ---------- Values
   //        ------ ------- Types

이와 같이 두 공간사이에서 다른 의미를 가지는 코드 패턴들이 있다.

패턴타입
this다형성(polymorphic) this자바스크립트의 this
ㄴ 서브클래스의 메서드 체인을 구현할 때 유용
&과 pipe인터섹션, 유니온ANDOR 비트 연산
constas const는 리터럴 또는 리터럴 표현식의 추론된 타입새 변수 선언
extends서브 타입 또는 제너릭 타입의 한정자를 정의할 수 있음서브 클래스 한정자 정의
interface A extends B, Generic<T extends number>class A extneds B
in매핑된(mapped)타입에 등장for (key in object)

자바스크립트에서는 객체 내의 각 속성을 로컬 변수로 만들어주는 구조 분해(descructuring) 할당을 사용할 수 있다. 그런데, 타입스크립트에서 구조 분해 할당을 하면 이상한 오류가 발생한다.

interface Person {
  first: string;
  last: string;
}
function email({
  person: Person,
       // ~~~~~~ Binding element 'Person' implicitly has an 'any' type
  subject: string,
        // ~~~~~~ Duplicate identifier 'string'
        //        Binding element 'string' implicitly has an 'any' type
  body: string}
     // ~~~~~~ Duplicate identifier 'string'
     //        Binding element 'string' implicitly has an 'any' type
) { /* ... */ }

값의 관점에서 Personstring이 해석되었기 때문에 발생한 오류이다. 문제를 해결하려면 다음과 같이 타입과 값을 구분해야 한다.

function email(
  {person, subject, body}: {person: Person, subject: string, body: string}
) {
  // ...
}

ITEM 09 타입 단언보다는 타입 선언을 사용하기

타입스크립트에서 변수에 값을 할당하고 타입을 부여하는 방법은 두가지가 있다.

interface Person { name: string };

const alice: Person = { name: 'Alice' };  // Type is Person
const bob = { name: 'Bob' } as Person;  // Type is Person

as Persion으로 표기한 형태가 타입 단언이다. 이렇게 타입을 지정하면 타입스크립트가 추론한 타입이 있더라도 Person으로 간주한다.

이 방법 외에 !를 사용해서 null이 아님을 단언하는 경우도 있다.

const elNull = document.getElementById('foo');  // Type is HTMLElement | null
const el = document.getElementById('foo')!; // Type is HTMLElement

단언문은 컴파일 과정 중에 제거되므로, 타입 체커는 알지 못하지만 그 값이 null이 아니라고 확신할 수 있을 때 사용해야 한다. 그렇지 않다면 null인 경우를 체크하는 조건문을 사용해야 한다.

타입 단언문을 사용할 때는 타입스크립트보다 타입 정보를 더 잘알고 있는 상황에서 사용하는 것이 좋다.

ITEM 10 객체 래퍼 타입 피하기

자바스크립트의 기본형 값들에 대한 일곱 가지 타입(string, number, boolean, null, undefined, symbol, bigint)와 객체 래퍼(String, Number, Boolean, Symbol, BigInt)가 항상 동일하게 동작하는 것은 아니다. 예를 들어, String객체는 오직 자기 자신하고만 동일하다.

타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링한다.

function isGreeting(phrase: String) {
  return [
    'hello',
    'good day'
  ].includes(phrase);
          // ~~~~~~
          // Argument of type 'String' is not assignable to parameter
          // of type 'string'.
          // 'string' is a primitive, but 'String' is a wrapper object;
          // prefer using 'string' when possible
}
profile
고양이 두마리를 모시고 있는 프론트엔드 코더(?)

2개의 댓글

comment-user-thumbnail
2023년 1월 14일

타입스크립트를 제대로 파고 들면 정말 공부할 게 많은 것 같아요.

1개의 답글