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 배열을 매개변수로 넣을 수 있다.