이펙티브 타입스크립트: 2장 타입스크립트의 타입 시스템

아이템 6-9

이펙티브 타입스크립트

동작 원리의 이해와 구체적인 조언 62가지

이펙티브 타입스크립트 표지

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

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

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

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

타입스크립트 컴파일러를 실행하는 것이 주된 목적이지만, 타입스크립트 서버 또한 ‘언어 서비스’를 제공한다는 점에서 중요합니다.

언어 서비스

  • 코드 자동 완성
  • 명세(사양,specification) 검사
  • 검색
  • 리팩터링



편집기마다 조금씩 다르지만 보통의 경우 마우스 커서를 대면 타입스크립트가 그 타입을 어떻게 판단하고 있는지 확인할 수 있습니다. num변수의 타입을 number라고 직접 지정하지는 않았지만, 타입스크립트는 10이라는 값을 보고 그 타입을 알아냅니다. 2-1 vscode 편집기 화면

함수의 타입도 추론할 수 있습니다. 주목할 점은 추론된 함수의 반환 타입이 number라는 것입니다. 2-2 함수타입추론

function logMessage (message: string | null) {
  if(message){
      message
  }
}

특정 시점에 타입스크립트가 값의 타입을 어떻게 이해하고 있는지 살펴보는 것은 타입 넓히기와 좁히기의 개념을 잡기 위해 꼭 필요한 과정입니다. 조건문의 분기에서 값의 타입이 어떻게 변하는지 살펴보는 것은 타입 시스템을 연마하는 매우 좋은 방법이고 편집기가 도움을 줄 것입니다.

조건문 외부에서 message의 타입은 string | null 이지만 내부에서는 string이다. 2-3 조건문 내부

언어 서비스는 라이브러리와 라이브러리의 타입 선언을 탐색할 때 도움이 됩니다. 코드 내에서 fetch 함수가 호출되고, 이 함수를 더 알아보길 원한다고 한다면 편집기에서는 ‘Go to Definition(정의로 이동)’ 옵션을 제공합니다

const response = fetch('https://www.dgmunit1.com/')

2-6 Go to Definition

‘Go to Definition(정의로 이동)’ 옵션을 선택하면 타입스크립트에 포함되어 있는 DOM타입 선언인 lib.dom.d.ts로 이동하고 fetch함수가 promise를 반환하고 두 개의 매개변수를 받는다는 것을 알 수 있습니다. 또 RequestInfo도 동일한 방법으로 탐색을 할 수 있습니다. 이렇게 더 많은 타입을 탐색하다 보면, 타입 선언이 처음에는 이해하기 어렵지만 타입스크립트가 무엇을 하는지, 어떻게 라이브러리가 모델링되었는지, 살펴볼 수 있게 도와줍니다. 2-6-3

메뉴에서 ‘Go to Definition(정의로 이동)’ 옵션을 누르지않고도 마우스를 올리면 편집기는 해당 함수에 대한 정보를 보여줍니다. 2-6-2

요약

  • 편집기에서 타입스크립트 언어 서비스를 적극 활용해야 합니다.
  • 편집기를 사용하면 어떻게 타입 시스템이 동작하는지, 그리고 타입스크립트가 어떻게 타입을 추론하는지 개념을 잡을 수 있습니다.
  • 타입스크립트가 동작을 어떻게 모델링하는지 알기 위해 타입 선언 파일을 찾아보는 방법을 터득해야 합니다.





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

never

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



리터럴(literal)타입

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

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



유니온(union)타입

두개 혹은 세 개로 묶은 타입 유니온 타입은 값 집합들의 ‘합집합’을 일컫습니다.

type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;

const a: AB = 'A'; // 정상
const c: AB = 'C'; // 'C'형식은 AB형식에 할당할 수 없습니다.

*** 타입체커 집합의 관점에서 타입 체커의 주요역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이라고 볼 수 있다.

type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
// OK, {"A", "B"} is a subset of {"A", "B"}:
const ab: AB = Math.random() < 0.5 ? 'A' : 'B';
const ab12: AB12 = ab;  // OK, {"A", "B"} 는 {"A", "B", 12} 의 부분 집합

declare let twelve: AB12;
const back: AB = twelve;
   // ~~~~ Type 'AB12' 는 'AB' 형식에 할당할 수 없습니다.
   //        Type '12' 는 'AB' 형식에 할당할 수 없습니다

*** 원소를 서술하는 방법

type Int = 1 | 2 | 3 | 4 | 5 | ...



인터섹션 타입

&연산자는 두 타입의 인터섹션(intersection, 교집합)을 계산합니다.

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'),
};  // 정상

타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용됩니다. 그리고 추가적인 속성을 가지는 값도 여전히 그 타입에 속합니다. 그래서 Person과 Lifespan을 둘다 가지는 값은 인터섹션 타입에 속합니다.

당연히 앞의 세가지(name, birth, death)보다 더 많은 속성을 가지는 값도 PersonSpan타입에 속합니다. 인터섹션 타입의 값은 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙

type K = keyof (Person | Lifespan) ; // 타입이 never

유니온 타입에 속하는 값은 어떠한 키도 없기 때문에, 유니온에 대한 keyof는 공집합입니다.


keyof (A&B) = (keyof A) | (keyof B) 
keyof (A|B) = (keyof A) & (keyof B) // A와 B 겹치는 것이 없으니 never



extend

interface Person {
  name: string;
}
interface PersonSpan extends Person {
  birth: Date;
  death?: Date;
}

타입이 집합이라는 관점에서 Extends의 의미는 ‘~에 할당 가능한’과 비슷하게 ‘~의 부분 집합’이라는 의미로 받아들일 수 있습니다. PersonSpan 타입의 모든 값은 문자열 name 속성을 가져야 합니다. 그리고 birth 속성을 가져야 제대로 된 부분 집합이 됩니다.


interface Vector1D { x: number; }
interface Vector2D extends Vector1D { y: number; }
interface Vector3D extends Vector2D { z: number; }

// 위 코드를 extends 없이 작성하면 이렇게
interface Vector1D { x: number; }
interface Vector2D { x: number; y: number; }
interface Vector3D { x: number; y: number; z: number; }

위 코드의 관계를 그림으로 나타내면 아래와 같습니다. extends없이 인터페이스로 코드를 재작성해 보면
부분집합, 서브타입, 할당 가능성의 관계가 바뀌지 않는다는 걸 명확히 알 수 있습니다. 2-7



요약

  • 타입을 값의 집합으로 생각하면 이해하기 편합니다.
  • 타입스크립트 타입은 엄격한 상속관계가 아니라 겹쳐지는 집합으로 표현됩니다.
  • 한객체의 추가적인 속성이 타입 선언에 언급되지 않더라도 그 타입에 속할 수 있습니다
  • 타입 연산은 집합의 범위에 적용됩니다.
  • ‘A는 B를 상속’, ‘A는 B에 할당 가능’, ‘A는 B의 서브타입’은 ‘A는 B의 부분 집합’과 같은 의미입니다.





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

심벌은 이름이 같더라도 속하는 공간에 따라 다른 것을 나타낼 수 있기 때문에 혼란스러울 수 있다.

interface Cylinder { // 타입
  radius: number;
  height: number;
}

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

function calculateVolume(shape: unknown) {
  if (shape instanceof Cylinder) {
    shape.radius
       // ~~~~~~ {} 형식에 radius 속성이 없습니다.'
  }
}

interface Cylinder의 Cylinder는 타입으로 쓰입니다. const Cylinder에서 Cylinder와 이름은 같지만 값으로 쓰입니다. 이 두 개는 서로 아무런 관련도 없습니다. 상황에 따라서 타입으로 쓰일 수도 있고, 값으로 쓰일 수도 있습니다. 이런 점이 가끔 오류를 발생시킵니다.

instanceof를 이용해 shapeCylinder타입인지 체크하려고 했는데 instanceof는 자바스크립트의 런타임 연산자이고, 값에 대해서 연산을 합니다. 그래서 instanceof Cylinder는 타입이 아니라 함수를 참조합니다.



type, interface 다음에 나오는 심벌은 ‘타입’, const, let 선언에 쓰이는 것은 ‘값’

type T1 = 'string literal';   // 타입
type T2 = 123;                // 타입
const v1 = 'string literal';  // 값
const v2 = 123;               // 값 

자바스크립트 코드 컴파일 과정에서 타입 정보는 제거되기 때문에, 심벌이 사라진다면 그것은 타입에 해당되는 될 것 입니다. T1, T2는 사라지고 v1,v2만 남아있게됩니다.

const v1 = 'string literal';  // 값
const v2 = 123;               // 값 



타입선언(:), 단언문(as) 다음에 나오는 심벌은 ‘타입’, = 다음에 나오는 모든 것은 ‘값’

interface Person {
  first: string;
  last: string;
}
const p: Person = { first: 'Jane', last: 'Jacobs' };
//    -           --------------------------------- 값
//       ------ 타입



두 공간 사이에서 다른 의미를 가지는 코드 패턴

타입
this 자바스크립트의 this 다형성(polymorphic) this 로 서브 클래스의 메서드 체인을 구현할 때 유용
&와ㅣ AND 와 OR 비트 연산 인터섹션과 유니온
const 새 변수 선언 as const는 리터럴 또는 리터럴 표현식의 추론된 타입을 바꿈
extends class에서 상속할 때 서브 클래스 또는 서브타입 또는 제너릭 타입의 한정자
in for in, key in object 루프 또는 매핑된 타입



요약

  • 타입스크립트 코드를 읽을 때 타입인지 값인지 구분하는 방법을 터득해야 합니다
  • 모든 값은 타입을 가지지만, 타입은 값을 가지지 않습니다. type과 interface같은 키워드는 타입 공간에만 존재합니다
  • class나 enum 같은 키워드는 타입과 값 두 가지로 사용될 수 있습니다
  • ‘foo’는 문자열 리터럴이거나, 문자열 리터럴 타입일 수 있습니다.
  • typeof, this 그리고 많은 다른 연산자들과 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용될 수 있습니다.





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

interface Person { name: string };

const alice: Person = { name: 'Alice' };  // 타입은 Person : 타입선언
const bob = { name: 'Bob' } as Person;  // 타입은 Person : 타입단언

두가지 방법은 결과가 같아 보이지만 그렇지 않습니다. 첫번째는 alice: Person은 변수에 ‘타입 선언’을 붙여서 그 값이 선언된 타입임을 명시 두번째는 as Person은 ‘타입 단언’을 수행합니다. 타입스크립트가 추론한 타입이 있더라도 Person 타입으로 간주 타입 단언보다는 타입선언을 사용하는게 낫다.

interface Person { name: string };
const alice: Person = {}; //  오류, ~~~~~ 'Person' 유형에 필요한 'name'속성이 {} 유형에 없습니다.
const bob = {} as Person;  // 오류 없음

타입 선언은 할당되는 값이 해당 인터페이스를 만족하는지 검사합니다. 타입 단언은 강제로 타입을 지정했으니 타입 체커에게 오류를 무시하라고 하는 것 입니다.

타입 단언이 꼭 필요한 경우가 아니라면, 안전성 체크도 되는 타입선언을 사용하는 것이 좋습니다.




화살표 함수의 타입 선언은 추론된 타입이 모호할 때가 있습니다.

다음 코드에 Person 인터페이스를 사용하고 싶다고 가정해보겠습니다.

interface Person { name: string };

const people = ['alice', 'bob', 'jan'].map(name => ({name}));
// Person[]을 원했지만 결과는 { name: string; }[]... 
const people = ['alice', 'bob', 'jan'].map(
  name => ({name} as Person)
); // Type is Person[]

name에 타입 단언을 쓰면 문제가 해결되는 것 처럼 보였지만 타입 단언을 사용하면 런타임에 문제가 발생하게 됩니다. {} 안에 name이 없어도 오류를 잡아낼 수 없습니다.


const people = ['alice', 'bob', 'jan'].map(name => {
  const person: Person = {name};
  return person
}); // Type is Person[]

단언문을 쓰지 않고, 다음과 같이 화살표 함수 안에 타입과 함께 변수를 선언하는 것이 가장 직관적입니다.

const people = ['alice', 'bob', 'jan'].map(
  (name): Person => ({name})
);

코드를 좀 더 간결하게 보이기 위해 변수 대신 화살표 함수의 반환 타입을 선언 이 코드는 위의 화살표 함수 안에 타입과 함께 변수를 선언한 것과 동일한 체크를 수행합니다.

const people: Person[] = ['alice', 'bob', 'jan'].map(
  (name): Person => ({name})
);

최종적으로 원하는 타입을 직접 명시하고, 타입스크립트가 할당문의 유효성을 검사하게 합니다.

타입 단언이 꼭 필요한 경우

  • 타입체커가 추론한 타입보다 여러분이 판단한 타입이 더 정확할 때 타입 단언을 사용한다.
  • 타입스크립트는 DOM에 접근할 수 없기 때문에 DOM을 조작하는 경우 타입단언을 사용
  • *! 단언문으로 null이 아님을 단언하는 경우

* Null아님 단언 연산자(접미사 !)

TypeScript에서는 명시적인 검사를 하지 않고도 타입에서 null과 undefined를 제거할 수 있는 특별한 구문을 제공합니다. 표현식 뒤에 !를 작성하면 해당 값이 null 또는 undefined가 아니라고 타입 단언하는 것입니다.

다른 타입 단언과 마찬가지로 이 구문은 코드의 런타임 동작을 변화시키지 않으므로, !연산자는 반드시 해당 값이 null 또는 undefined가 아닌 경우에만 사용해야 합니다.



요약

  • 타입 단언(as Type)보다는 타입 선언(: Type)을 사용해야 합니다
  • 화살표 함수의 반환 타입을 명시하는 방법을 터득해야 합니다
  • 타입스크립트보다 타입 정보를 더 잘 알고 있는 상황에서는 타입 단언문과 null아님 단언문을 사용하면 됩니다.

읽고 난 후..

제가 이해할 수 있게 도움을 준 팀 멤버들 최고👍 감사합니다🙏

디지엠유닛원 주식회사

  • 대표이사 권혁태
  • 개인정보보호책임자 정경영
  • 사업자등록번호 252-86-01619
  • 주소
    서울특별시 금천구 가산디지털1로 83, 6층 601호(가산동, 파트너스타워)
  • 이메일 unit1@dgmit.com