[Effective Typescript] item6~8 정리

김유진·2023년 3월 27일
1

Effective-TypeScript

목록 보기
3/28
post-thumbnail

item6: 편집기를 사용하여 타입 시스템 탐색하기

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

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

이 두가지를 실행할 수 있으므로, 타입스크립트도 언어 서비스를 제공한다고 할 수 있다.
언어 서비스는 코드 자동 완성, 명세, 검사, 검색, 리팩터링이 포함된다.

타입스크립트 편집기를 사용하면서 가장 좋은 점은, 심벌 위에 마우스 커서를 대면 타입스크립트가 타입을 어떻게 판단하는지 실시간으로 확인이 가능하다
이 때 타입이 기대되는 것과 다르다면 직접 값을 반환하는 것을 지정할 수 있다.

이러한 값의 추론 정보는 디버깅에 굉장히 유용하게 사용될 수 있다.

이런 에러들도 잡아낼 수 있다.

function getElement(elOrId: string|HTMLElement|null): HTMLElement {
  if(typeof elOrId === "object") {
    return elOrId;
  }
  // 'HTMLElemnt | null 형식은 'HTMLElement' 형식에 할당할 수 없습니다

첫번째 분기문의 의도는 HTMLElement라는 객체를 골라내는 것이 목적이다. 그런데, elOrId가 분기문 안에서는 null이 될 수 있고 널 형식은 object 이므로 분기문이 에러가 생깁니다. 그래서 Null을 확인할 수 있는 장치를 마련해둬야 합니다.

마지막으로 타입스크립트가 어떻게 모델링을 하고 있는지, 편집기의 Go to Definition 기능을 이용하면 쉽게 모델링된 모습을 확인하여, 타입스크립트가 어떻게 동작하는지를 쉽게 확인할 수 있다.

item7: 타입이 값들의 집합이라고 생각하기

타입은 할당 가능한 값들의 집합이라고 생각해야 한다. 이러한 집합은 타입의 범위라고 부르기도 하는데, 범위에 대해서 타입을 생각해보자.
가장 먼저, 제일 작은 집합은 아무것도 포함하지 않는 공집합이다. 이는 타입스크립트에서 never 타입으로 사용되며, 이로 선언된 변수의 범위는 공집합의 타입을 가지기 때문에 아무런 값도 할당할 수 없다.

다음으로 작은 집합은 한 가지 값만 포함하는 타입이다. 이는 유닛타입이라고 불리는 리터럴타입이다.

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

이러한 타입을 두개 혹은 세개로 묶으려면 유니온타입을 사용한다.

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

세 개 이상의 타입을 묶을 때에도 동일하게 |을 사용하여 이어주면 된다. 이는 집합들의 합집합을 의미한다.

const a: AB = 'A'; //정상 'A'는 집합 {'A', 'B'}의 원소이다.
const c: AB = 'C';
//~ '"C"' 형식은 'AB' 형식에 할당할 수 없습니다.

이렇게 타입스크립트의 에러 체크 과정에서 할당 가능한이라는 문구와 함께 안내를 하는 것을 볼 수 있다. 이는 집합의 관점에서, '~의 원소(값과 타입과의 관계)' 또는 '~의 부분집합(두 타입의 관계)'을 의미하는 것이다.
집합의 관점에서 위의 에러는 C 유닛 타입이 다른 집합의 부분 집합에도 속하지 않으므로 에러가 발생하는 것이다.

집합의 연산, 그리고 타입

인터섹션(교집합)

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

& 연산자는 두 타입의 인터섹션을(교집합)을 계산한다. PersonSpan이 공집합 타입으로 예상하기 쉬운데, 타입 연산자는 인터페이스의 속성이 아니라 값의 집합에 적용된다. 그렇기 때문에, 결과적으로 PersonSpan타입은 아래와 같은 결과를 가진다.

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

당연히, 구조적 타이핑의 원리에 따라서 위의 세 가지 속성보다 더 많은 속성을 가지는 값도 PersonSpan속성에 속할 수 있다.

유니온

type K = keyo (Person | Lifespan); 

유니온 타입에 속하는 값은 어떠한 키도 없기 때문에, 유니온에 대한 keyof는 공집합이다.
정리하자면 아래와 같이 정리 가능하다.

keyof(A&B) = (keyof A) | (keyof B)
keyof(A|B) = (keyof B) & (keyof A)

인터페이스에 관한 것과 값에 대한 연산이 조금 다르게 작동하는 것을 헷갈려하면 안되겠다.

extends 키워드

집합 간의 상속 관계를 표현하기 위하여 일반적으로 extend 키워드를 사용한다.

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

extends의 의미는 ~에 할당 가능한 과 비슷한 의미를 가지고 있으며, '~의 부분 집합' 이라는 의미로 받아들일 수 있다. 이를 밴 다이어그램으로 표현하면, PersonSpanPerson의 서브타입이다.

한정자로 쓰이는 extends 키워드

function getKey<K extends string>(val: any, key: K) {
  //...
}

이 코드에서 Kstring의 부분 집합 범위를 가지는 어떠한 타입이 된다.

getKey({}, 'x');
getKey({}, Math.random() < 0.5 ? 'a' : 'b');
getKey({}, document.title);
getKey({}, 12);
	// ~~ '12' 형식의 인수는 'string' 형식의 매개변수에 할당될 수 없습니다.

할당될 수 없습니다의 의미는, 상속할 수 없습니다라고 바꿀 수 있고, 상속의 범위에서 보면 마지막 줄에서 왜 오류가 발생하는지 쉽게 판단할 수 있습니다.

interface Point {
  x: number;
  y: number;
}
type PointKeys = keyof Point;

function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] }
	//..
}
const pts: Point[] = [{x: 1, y: 1}, {x: 2, y: 0}];
sortBy(pts, 'x'); //정상 'x'는 'x'|'y'를 상속
sortBy(pts, 'y'); //정상 'y'는 'x'|'y'를 상속
sortBy(pts, Math.random() < 0.5 ? 'x' : 'y' );
sortBy(pts, 'z');
	// ~~ '"z"' 형식의 인수는 '"x" | "y"' 형식의 매개변수에 할당될 수 없습니다.

결론은 타입을 집합 관점에서 바라보기

타입들이 엄격한 상속 관계가 아닐 때는 집합 스타일로 이들을 바라보는 것이 더욱 유용하다.
string|numberstring | Date 사이의 인터섹션은 공집합이 아닌, string이다. 타입이 집합이라는 관점은 배열과 튜플의 관계도 매우 명확하게 만든다.
이것 때문에 머리 아팠던 적이 한두번이 아닌데, 덕분에 명쾌하게 알 수 있었다.

const list = [1, 2]; //타입은 number[]
const tuple: [number, number] = list;
	// ~~ 'number[]' 타입은 '[number, number]' 타입의 0, 1 속성에 없습니다.

이 친구가 성립하지 않는 반례를 들자면, 숫자 쌍인 [number, number]은 빈 리스트 [][1] 과 같은 반례가 존재하고 이를 포함하지 못한다. 그래서 number[][number, number]부분 집합이 아니므로, 할당할 수 없는 것이다.
그러나, 반대로 할당하면 부분집합에 속하기 때문에 제대로 동작한다.

const triple: [number, number, number] = [1, 2, 3];
const double: [nubmer, number] = triple;
	//'[number, number, number]'형식은 '[number, number]' 형식에 할당할 수 없습니다. 'length' 속성의 형식이 호환되지 않습니다.

구조적 타이핑의 관점에서 생각하면 위의 코드는 에러 없이 잘 수행되어야 한다.
타입스크립트는 숫자의 쌍을 {0: number, 1: number} 이렇게 모델링하는 것이 아니라, {0: number, 1: number, length: 2}와 같이 모델링을 진행한다. 그래서 length의 값이 맞지 않아서 할당문에 오류가 발생하는 것이다.

결론적으로 이번 아이템이 전달하고자 하는 바를 정리하면 아래와 같다

'A는 B를 상속', 'A는 B에 할당 가능', 'A는 B의 서브타입' 은 'A는 B의 부분 집합'과 같은 의미이다.

item8: 타입 공간과 값 공간의 심벌 구분하기

타입스크립트의 심벌(symbol)은 타입 공간이나 값 공간 중의 한 곳에 존재한다.
그래서 이 둘의 위치를 혼동하게 되면 혼란을 초래할 수 있으므로 이 둘을 잘 구분하여 사용할 수 있어야 한다.

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

interface Cylinder는 타입으로 사용된다. const Cylinder으로 쓰인다.
즉 정리하자면 Cylinder는 값으로도 쓰일 수 있고, 타입으로 쓰일 수도 있다는 것이다.
이런 점이 오류를 발생시킬 수 있다.

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

instanceof는 자바스크립트의 런타임 연산자이고, 값에 대하여 연산을 진행하기 때문에 Cylinder가 값으로 (함수로) 참조되어 이런 에러가 발생하는 것이다.
심벌이 타입인지 값인지는 한눈에 봐서 잘 알 수 없으므로, 문맥을 살펴야 한다.
일반적으로, type이나 interface 다음에 오는 것은 타입, constlet 선언에 쓰이는 것은 으로 평가할 수 있다.
이 부분이 헷갈린다면 타입스크립트 플레이그라운드를 사용하여 타입 심볼을 지울 수 있다. 이 사이트는 자바스크립트 표현으로 바꿔주기 때문이다.

타입과 값은 번갈아 나온다.

interface Person {
  first: string;
  last: string;
}
const p: Person = {first: 'Jane', last: 'Jacos' };

이렇게 함수와 타입이 번갈아 나올 수 있다.

타입과 값이 모두 가능한 경우

class

classenum은 타입과 값 두가지가 모두 가능한 예약어이다.

class Cylinder { 
  radius= 1;
  height= 1;
}
function calculateVolume(shape: unknown) {
  if (shape instanceof Cylinder) {
    shape //Cylinder 타입으로 판단됨.
    shape.radius
  }
}

클래스가 타입으로 쓰일 때는 형태(속성, 메서드)가 사용되는 반면, 값으로 쓰일 때는 생성자가 사용된다. 이 예시에서는 Cylinder가 타입으로 사용된 것이다.

typeof

연산자 중에서도 타입에서 쓰일 때와 값으로 쓰일 때 다른 역할을 하는 기능이 있다.
대표적으로 typeof타입의 관점에서 값을 읽어 타입스크립트 타입을 반환한다. 값의 관점으로는 자바스크립트 런타임의 Typeof 연산자로 작동한다. 이렇게 상황에 따라 다르게 동작할 수 있다.

const v = typeof Cylinder; //값이 "function"
type T = typeof Cylinder; //타입이 "typeof Cylinder"

타입의 속성을 얻으려면

속성 접근자인 []는 타입을 쓰일 때 동일하게 동작한다.

obj['field']obj.field는 값은 동일해 보일 수 있어도, 타입의 속성을 얻을 때는 반드시 obj['field']를 사용해야 한다.

에러 핸들링

타입스크립트 코드가 잘 동작하지 않는다면, 타입 공간과 값 공간을 혼동해서 잘못 작성하였을 가능성이 크다. 구조 분해 할당을 이용하여 아래 코드를 바꾸어봤다고 하자.

function email (options: {person: Person, subject: string, body: string}) { //...}
을 바꾸어서
function email({person: Person, subject: string, body: string}) }
	//...
}

타입스크립트에서 구조 분해 할당을 하면 위의 코드는 아래와 같은 에러를 반환한다.

바인딩 요소 'Person'에 암시적으로 'any' 형식이 있습니다.
'string' 식별자가 중복되었습니다.
바인딩 요소 'string'	에 암시적으로 'any'형식이 있습니다.

값의 관점에서 Person과 string이 해석되었기 때문에, 오류가 발생한다.
Person이라는 변수명과, string이라는 이름의 변수를 생성하였기 때문이다.
문제를 해결하기 위해서는 타입과 값을 구분하여 적어야 한다.

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

매개변수에 대한 타입을 정확하게 지정해주어야 에러가 발생하지 않는다.

0개의 댓글