✅ 1장 TS 알아보기
아이템 1: TS와 JS의 관계
“타입스크립트는 자바스크립트의 상위집합(superset)이다”
-
그렇기 때문에 JS 코드는 이미 TS다.
- 기존 JS 코드를 타입스크립트로 마이그레이션하는데 큰 이점
- 타입 구문을 사용하는 순간부터 JS는 TS 영역으로 들어가게 됨
-
타입 시스템에서는 런타임에 오류를 발생시킬 코드를 미리 찾아낸다.
const city = [ { name: 'jeon-ju', food: 'kong-na-mul-kook-bob' }, { name: 'seoul', food: 'none' }, // ... ]; for (const localCity of city) { console.log(localCity.size); // JS에서는 undefined, TS에서는 에러 }
-
타입을 명시적으로 선언하여 의도를 분명하게 하면 오류를 구체적으로 알 수 있다.
interface City { name: string; food: string; } const city: City[] = [ { name: 'jeon-ju', food: 'kong-na-mul-kook-bob' }, { name: 'seoul', food: 'none' }, ]; // 🚨 Error // 'City' 형식에 'size'이 없습니다. for (const localCity of city) { console.log(state.size); }
아이템 2: 타입스크립트 설정
-
tsconfig.json
으로 타입스크립트 설정 작성{ "compilerOptions": { // ... } }
-
noImplicitAny
: 변수들이 미리 정의된 타입을 가져야 하는지 여부를 제어(any
타입을 사용하면 에러 설정)function add(a, b) { return a + b; } // 이를 암시적 any라고 부른다 // noImplicitAny가 설정되었다면 오류 발생 function add(a: any, b: any): any;
-
strictNullChecks
:null
과undefined
가 모든 타입에서 허용되는지 설정// strictNullChecks 해제 시 const x: number = null; // 정상 // strictNullChecks 설정 시 // 🚨 에러: 'null' 형식은 'number' 형식에 할당할 수 없습니다. const x: number = null;
아이템 3: 코드 생성과 타입은 관계가 없음
-
TS 컴파일러는 2가지 역할을 수행
- 최신 TS,JS를 브라우저에서 동작할 수 있도록 구버전 JS로 트랜스파일 함
- 코드의 타입 오류를 체크 함
-
타입 오류가 있는 코드도 컴파일 가능
- 컴파일은 타입 체크와 독립적으로 동작하기 때문
- 작성한 TS가 유효한 JS라면 TS 컴파일러는 컴파일 진행
-
런타임에는 타입 체크가 불가능
-
TS의 타입은 ‘제거 가능’ 즉, JS로 컴파일되는 과정에서 모든 인터페이스, 타입, 타입 구문은 그냥 사라짐
-
런타임에 타입 정보를 유지하는 방법
-
특정 속성이 존재하는지 체크
-
태그
기법 : 런타임에 접근 가능한 타입 정보를 명시적으로 저장interface Square { kind: 'square'; width: number; } interface Rectangle { kind: 'rectangle'; height: number; width: number; } // '태그된 유니온(tagged union)' type Shape = Square | Rectangle; function calculateArea(shape: Shape) { if (shape.kind === 'rectangle') { shape; // 타입이 Rectangle return shape.width * shape.height; } else { shape; // 타입이 Square return shape.width * shape.width; } }
-
-
-
타입 연산은 런타임에 영향을 주지 않음
- 값을 정제하기 위해서는 런타임의 타입을 체크해야 하고 JS 연산을 통해 변환을 수행해야 함
-
런타임 타입은 선언된 타입과 다를 수 있음
switch~case
구문의default
구문- API 요청의 반환값을 사용하는 경우
-
TS 타입으로는 함수를 오버로드할 수 없음
function add(a: number, b: number) { return a + b; } // 🚨 에러: 중복된 함수 구현입니다. function add(a: string, b: string) { return a + b; } // 🚨 에러: 중복된 함수 구현입니다.
- TS 함수 오버로딩은 타입 수준에서만 가능(구현체는 불가)
function add(a: number, b: number): number; function add(a: string, b: string): string;
- TS 함수 오버로딩은 타입 수준에서만 가능(구현체는 불가)
-
TS 타입은 런타임 성능에 영향을 주지 않음
- 타입과 타입 연산자는 JS 변환 시점에 제거되기 때문
- 런타임 오베허드가 없는 대신, TS 컴파일러는 ‘빌드타임’ 오버헤드가 있음
- TS 컴파일하는 코드는 오래된 런타임 환경을 지원하기 위해 호환성을 높이고 성능 오버헤드를 감안할지, 호환성을 포기하고 성능 중심의 네이티브 구현체를 선택할지의 문제에 맞닥뜨릴 수도 있음
아이템 4: 구조적 타이핑에 익숙해지기
-
JS는 본질적으로 덕 타이핑(duck typing) 기반
- 덕 타이핑 : 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식
-
TS도 JS 처럼 덕 타이핑 동작을 그대로 모델링 함
interface Vector2D { x: number; y: number; } function calculateLength(v: Vector2D) { return Math.sqrt(v.x * v.x + v.y * v.y); } // Vector2D 인터페이스를 확장하며, name 프로퍼티를 추가로 가지고 있음 // 구조적 타이핑 때문에, Vector2D 인터페이스와 호환 // NamedVector 인터페이스의 x,y와 Vector2D의 x와 y 동일 // 따라서 NamedVector 인터페이스를 구현한 객체도 // calculateLength 함수의 인자로 사용할 수 있음 interface NamedVector { name: string; x: number; y: number; } let v: NamedVector = { name: 'vec1', x: 3, y: 4 }; calculateLength(v); // 5
-
구조적 타이핑을 사용하면 유닛 테스트를 쉽게 할 수 있음
-
TS는 테스트 DB가 특정 인터페이스를 충족하는지 확인
-
추상화(DB)를 함으로써, 로직과 테스트를 특정한 구현으로부터 분리 가능
interface DB { runQuery: (sql: string) => any[]; } function getAuthors(database: DB): Author[] { const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`); return authorRows.map((row) => ({ first: row[0], last: row[1] })); }
-
아이템 5: any 타입 지양하기
-
any
타입에는 타입 안정성이 없음let age: number; age = '12' as any; // 정상 age += 1; // 런타임에 정상, 🚨 age는 '121'
-
any
는 함수 시그니처를 무시 함function calculateAge(birthDate: Date): number { // ... } let birthDate: any = '1592-05-11'; calculateAge(birthDate); // 정상 (🚨 추후 에러 발생 가능)
-
any
타입에는 언어 서비스가 적용되지 않음- IDE의 자동완성 기능과 적절한 도움말 제공 불가
-
any
타입은 코드 리팩터링 때 버그를 감춤any
가 아닌 구체적인 타입을 사용하여 타입 체커가 오류를 발견하도록 해야 함
-
any
는 타입 설계를 감춤- 애플리케이션 상태 등의 객체 설계 시
any
사용을 지양
- 애플리케이션 상태 등의 객체 설계 시
-
any
는 타입시스템의 신뢰도를 떨어뜨림- 사람은 항상 실수를 함
any
타입을 쓰지 않으면 런타임에 발견될 오류를 미리 잡을 수 있고 신뢰도를 높일 수 있음
✅ 2장 TS 타입 시스템
아이템 6: 편집기를 사용하여 타입 시스템 탐색하기
- TS에서 실행할 수 있는 프로그램
- TS 컴파일러(tsc)
- 단독 실행 가능한 TS 서버
- TS 서버에서 제공하는 언어 서비스를 사용 권장
- 많은 편집기에서 TS가 그 타입을 어떻게 판단하는지 확인 가능
- 편집기 타입 오류를 살펴보는 것도 타입 시스템을 파악하는 데 좋은 방법
- 라이브러리와 라이브러리의 타입 선언
- Go to Definition 옵션으로
d.ts
에서 타입 정의 확인 가능
- Go to Definition 옵션으로
아이템 7: 타입이 값들의 집합이라고 생각하기
-
런타임시 모든 변수는 JS로 정해진 고유한 값 존재
-
코드가 실행되기 전 TS가 오류를 체크하는 순간에는 타입을 가지고 있으며, 이는 할당 가능한 값들의 집합
-
집합의 종류
never
: 아무것도 포함하지 않는 공집합(아무 값도 할당 불가) cf)void
는undefined
반환// 🚨 '12' 형식은 'never' 형식에 할당할 수 없습니다. const x: never = 12;
- 리터럴(유닛)타입 : 한 가지 값만 포함하는 타입
type A = 'A';
- 유니온 타입 : 두 개 혹은 세 개 값 포함하는 집합들의 합집합
type AB = 'A' | 'B';
-
할당 가능
하다는 뜻 -> 부분 집합// 'A'는 집합 {'A', 'B'}의 원소 const a: AB = 'A';
-
실제 다루게 되는 타입은 대부분 범위가 무한대
type Int = 1 | 2 | 3 | 4 | 5; // | ...
-
원소를 서술하는 방법
interface Identified { id: string; }
-
타입(값의 집합)
-
&
연산자는 두 타입의 인터섹션(교집합)을 계산 -
|
연산자는 두 인터페이스의 유니온, 교집합이 없는 두 개 이상의 타입에서 사용 시 주의interface Person { name: string; } interface Lifespan { birth: Date; death?: Date; } type PersonSpan = Person & Lifespan; type K = keyof (Person | Lifespan); // 타입이 never
-
-
extends
: ~에 할당 가능한, ~의 부분집합- 서브타입 : 어떤 집합이 다른 집합의 부분집합
interface Vector1D { x: number; } // Vector2D는 Vector1D의 서브타입 interface Vector2D extends Vector1D { y: number; } // Vector3D는 Vector2D의 서브타입 interface Vector3D extends Vector2D { z: number; }
-
제네릭에서
extends
// K는 집합 관점에서 string을 상속 // string 리터럴 타입, string 리터럴 타입의 유니온, string 자신을 포함 function getKey<K extends string>(val: string, key: K) { // ... }
-
타입이 집합이라는 관점에서 배열과 튜플의 관계 확인
// 타입은 number[] const list = [1, 2]; // 🚨 'number[]' 타입이 '[number, number]'타입 보다 큰 집합이여서 // 에러 발생 // Target requires 2 element(s) but source may have fewer const tuple: [number, number] = list;
-
트리플
const triple: [number, number, number] = [1, 2, 3]; // 🚨 숫자의 length값이 맞지 않기 때문에 할당문에 오류 발생 const double: [number, number] = triple;
-
타입이 값의 집합이라는 뜻은, 동일한 값의 집합을 가지는 두 타입은 같다는 의미
아이템 8: 타입 공간과 값 공간의 심벌 구분하기
-
TS 심벌(symbol)은 타입 공간이나 값 공간 중 한 곳에 존재
interface Cylinder { radius: number; height: number; } const Cylinder = (radius: number, height: number) => ({ radius, height });
- interface Cylinder는 타입, const Cylinder는 변수
- 일반적으로 type이나 interface 다음에 나오는 심벌은 타입, const나 let 선언에 쓰이는 것은 값
-
class
와enum
은 상황에 따라 타입과 값 두 가지 모두 가능-
클래스가 타입으로 쓰일 때는 형태(속성과 메서드)가 사용되는 반면, 값으로 쓰일 때는 생성자가 사용됨
// 타입으로 쓰인 Cylinder 클래스 class Cylinder { radius = 1; height = 1; } function calculateVolume(shape: unknown) { if (shape instanceof Cylinder) { shape; // 정상, 타입은 Cylinder shape.radius; // 정상, 타입은 number } }
-
-
typeof
: 타입에서 쓰일 때와 값에서 쓰일 때가 다름-
타입의 관점에서
typeof
는 값을 읽어서 TS 타입을 반환 -
값의 관점에서
typeof
는 JS 런타임의typeof
연산자를 반환(심벌의 런타임 타입을 가리킴)// 타입은 Person type T1 = typeof p; // 타입은 (p: Person, subject: string, body: string) => Response type T2 = typeof email; // 값은 'object' const v1 = typeof p; // 값은 'function' const v2 = typeof email;
-
-
클래스
// 타입이 typeof Cylinder type T = typeof Cylinder; declare let fn: T; const c = new fn(); // 타입이 Cylinder
InstanceType
제너릭을 사용해 생성자 타입과 인스턴스 타입 전환 가능type C = InstanceType<typeof Cylinder>; // 타입이 Cylinder
-
속성 접근자
[]
obj['field']
와obj.field
는 값이 동일하더라도 타입은 다를 수 있으므로, 타입의 속성을 얻을 때는obj['field']
를 지향
아이템 9: 타입 단언보다는 타입 선언을 사용하기
-
타입 단언은 오류를 발견하지 못 함
interface Person { name: string; } // 🚨 'Person' 유형에 필요한 'name' 속성이 '{}' 유형에 없습니다. const kay: Person = {}; const bob = {} as Person; // 오류 없음
- 속성을 추가할 때도 마찬가지(타입 선언문에서는 잉여 속성 체크가 동작하지만, 단언문에서는 적용되지 않음)
-
화살표 함수의 타입 선언
const people = ['kay', 'bob', 'jun'].map((name) => ({ name })); // Person[]을 원했지만 결과는 { name: string; }[]...
- 단언문 대신 화살표 함수의 반환 타입을 선언
// 타입은 Person[] const people = ['kay', 'bob', 'jun'].map((name): Person => ({ name }));
- 그러나 함수 호출 체이닝이 연속되는 곳에서는 체이닝 시작에서부터 명명된 타입을 가져야 오류가 정확하게 표시 됨
- 단언문 대신 화살표 함수의 반환 타입을 선언
-
타입 단언이 꼭 필요한 경우
- 타입 체커가 추론한 타입보다
개발자가 판단하는 타입이 더 정확할 때
document.querySelector('#myButton').addEventListener('click', (e) => { e.currentTarget; // 타입은 EventTarget // 타입은 HTMLButtonElement const button = e.currentTarget as HTMLButtonElement; });
- 타입 체커가 추론한 타입보다
-
!
문법을 사용해서null
이 아님을 단언하는 경우// 타입은 HTMLElement | null const elNull = document.getElementById('foo'); // 타입은 HTMLElement const el = document.getElementById('foo')!;
-
타입 단언문으로 임의의 타입 간에 변환
A
가B
의 부분집합(서브타입)인 경우 사용
아이템 10 : 객체 래퍼 타입 피하기
- JS는 기본형과 객체 타입을 서로 자유롭게 변환 가능(래퍼 객체)
string
기본형과String 래퍼 객체
가 항상 동일하게 동작하는 것은 아님String 객체
는 오직 자기 자신하고만 동일하다'hello' === new String('hello'); // false new String('hello') === new String('hello'); // false
- TS는 기본형과 객체 래퍼 타입을 별도로 모델링 함
// 🚨 'String' 형식의 인수는 'string' 형식의 매개변수에 할당될 수 없습니다. // 'String'은 Object 임 function isGreeting(phrase: String) { return ['hello', 'good day'].includes(phrase); }
string
은String 래퍼 객체
에 할당할 수 있지만,String 래퍼 객체
은string
에 할당할 수 없음- TS가 제공하는 타입 선언은 전부 기본형 타입
아이템 11: 잉여 속성 체크의 한계 인지하기
-
타입이 명시된 변수에 객체 리터럴을 할당할 때 TS는 해당 타입의 속성이 있는지, 그리고
그 외의 속성은 없는지
확인interface Room { numb: number; size: number; } const room = { numb: 1, size: 10, bed: 4, }; const secondRoom: Room = room; // 정상
- room 타입은 Room 타입의 부분 집합을 포함하므로, Room에 할당 가능하며 타입 체커도 통과 함
- 잉여 속성 체크는 할당 가능 검사와는 별도의 과정
-
TS는 런타임 오류 뿐 아니라, 의도와 다르게 작성된 코드까지 찾음
interface Options { title: string; darkMode?: boolean; } function createWindow(options: Options) { if (options.darkMode) { setDarkMode(); } } createWindow({ title: 'Spider Solitaire', darkmode: true, // 🚨 에러 darkMode 아님? });
- 런타임에 에러가 발생하지 않지만, TS에서 에러가 발생 함
-
Options
는 넓은 타입으로 해석 됨interface Options { title: string; darkMode?: boolean; } const o1: Options = document; // 정상 const o2: Options = new HTMLAnchorElement(); // 정상
document
와HTMLAnchorElement
의 인스턴스 모두 string 타입인 title 속성을 갖고 있기 때문에 할당문 정상
-
잉여 속성 체크는 객체 리터럴만 체크 함
interface Options { title: string; darkMode?: boolean; } // 🚨 에러 - darkMode 아님? const o1: Options = { darkmode: true, title: 'Ski Free' }; const intermediate = { darkmode: true, title: 'Ski Free' }; const o2: Options = intermediate; // 정상 // 타입 단언문을 사용할 때도 적용되지 않는다 const o3: Options = { darkmode: true, title: 'Ski Free' } as Options; // 정상
아이템 12: 함수 표현식에 타입 적용하기
-
TS는 함수 선언문이 아닌,
함수 표현식
을 권장- 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점 존재(시그니처)
type DiceRollFn = (sides: number) => number; const rollDice: DiceRollFn = (sides) => {};
- 함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점 존재(시그니처)
-
반복되는 함수 시그니처를 하나의 함수로 통합하여 불필요한 코드의 반복을 줄일 수 있음
- 라이브러리는 공통 함수 시그니처를 타입으로 제공 ex) 리액트
MouseEventHandler
- 라이브러리는 공통 함수 시그니처를 타입으로 제공 ex) 리액트
-
시그니처가 일치하는 다른 함수가 있을 때도 함수 표현식에 타입 적용 가능
-
ex)
fetch
함수// 타입이 Promise<Response> const responsePromise = fetch('/search?by=Kay');
-
응답의 데이터를 추출
async function getSearch() { const response = await fetch('/search?by=Kay'); const data = await response.json(); return data; }
- 이때
/search
가 존재하지 않는 API거나fetch
가 실패하는 경우 버그가 발생 함 - 상태 체크를 수행해 줄
checkedFetch
함수 작성 - 함수 선언문을 함수 표현식으로 바꾸고, 함수 전체에 타입을 적용
// lib.dom.d.ts declare function fetch( input: RequestInfo, init?: RequestInit ): Promise<Response>; const checkedFetch: typeof fetch = async (input, init) => { const response = await fetch(input, init); if (!response.ok) { throw new Error('Request failed: ' + response.status); } return response; }; ``;
- 이때
-
아이템 13: 타입과 인터페이스의 차이점 알기
-
인터페이스와 타입 모두 사용 가능한 경우
- 인덱스 시그니처
- 함수 타입
- 제너릭
type TPair<T> = { first: T; second: T; } interface IPair<T> = { first: T; second: T; }
-
인터페이스는 다른 타입을 포함할 수 있어 타입을 확장 할 수 있고 타입이 인터페이스를 포함 시킬 경우 인터페이스를 확장 할 수 있음
-
인터페이스가 타입을 확장하는 경우
interface Person { name: string; age: number; } interface Employee extends Person { salary: number; }
-
타입이 인터페이스를 확장하는 경우
interface Shape { color: string; area(): number; } type Circle = { radius: number; } & Shape;
-
-
인터페이스와 타입의 차이점
인터페이스
는 객체의 구조를 정의하기 위한 것으로 사용타입
은 객체, 변수, 함수 등의 값을 설명하기 위해 사용- 유니온 타입은 있지만 유니온 인터페이스는 없음
type AorB = 'a' | 'b';
-
유니온 타입 확장이 필요한 경우
type Input = { /* ... */ }; type Output = { /* ... */ }; interface VariableMap { [name: string]: Input | Output; }
-
유니온 타입에 추가 속성을 붙인 타입 만들기(인터페이스로 표현 불가)
type NamedVariable = (Input | Output) & { name: string };
-
튜플과 배열 타입
type Pair = [number, number]; type StringList = string[]; type NamedNumbs = [string, ...number[]]; // 인터페이스로 튜블과 비슷하게 구현(제한적, 튜플 메서드 사용 불가) interface Tuple { 0: number; 1: number; length: 2; } const t: Tuple = [10, 20]; // 정상
-
타입에는 없는 인터페이스의 보강 기능(선언 병합)
interface IState { name: string; capital: string; } interface IState { population: number; } const city: IState = { name: "Jeon-Ju", capital: "Jeon-Ju", population: 500,000, }; // 정상
-
TS는 여러 버전의 JS 표준 라이브러리에서 타입을 모아 병합 함
-
타입은 기존 타입에 추가적인 보강이 없는 경우에만 사용해야 함
-
복잡한 타입이라면 타입 별칭을, 간단한 객체 타입이라면 인터페이스를 사용(협업시 일관성 있게 사용하는 것이 중요)
아이템 14: 타입 연산과 제너릭 사용으로 반복 줄이기
-
타입에 이름 붙이기
-
타입이 반복적으로 등장하는 함수
function distance( a: { x: number; y: number }, b: { x: number; y: number } ) { return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); }
-
타입에 이름을 붙여 개선하기
interface Point2D { x: number; y: number; } function distance(a: Point2D, b: Point2D) { /* ... */ }
-
-
함수의 타입 시그니처 개선하기
-
몇몇 함수가 같은 타입 시그니처를 공유하는 경우
function get(url: string, opts: Options): Promise<Response> { /* ... */ } function post(url: string, opts: Options): Promise<Response> { /* ... */ }
-
해당 시그니처를 명명된 타입으로 분리하기
type HTTPFunction = (url: string, opts: Options) => Promise<Response>; function get: HTTPFunction = (url, opts) => { /* ... */ } function post: HTTPFunction = (url, opts) => { /* ... */ }
-
-
인터페이스를 확장하여 반복 제거하기
interface Person { firstName: string; lastName: string; } interface PersonWithBirthDate extends Person { birth: Date; }
-
이미 존재하는 타입을 확장하는 경우 인터섹션 연산자(&) 사용하기
type PersonWithBirthDate = Person & { birth: Date };
-
전체 애플리케이션의 상태를 표현하는
State
타입과 부분만 표현하는TopNavState
합치기interface State { userId: string; pageTitle: string; recentFiles: string[]; pageContents: string; } interface TopNavState { userId: string; pageTitle: string; recentFiles: string[]; }
-
매핑된 타입 사용하기
type TopNavState = { [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]; };
-
유틸 타입
Pick
사용하기type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
-
-
태그된 유니온에서 인덱싱하기
interface SaveAction { type: 'save'; } interface LoadAction { type: 'load'; } type Action = SaveAction | LoadAction; type ActionType = Action['type']; // 타입은 'save' | 'load'
-
타입을 선택적 필드를 포함하는 타입으로 변환하기
interface Options { width: number; height: number; color: string; label: string; } interface OptionsUpdate { width?: number; height?: number; color?: string; label?: string; }
-
매핑된 타입과
keyof
사용하기type OptionsUpdate = { [k in keyof Options]?: Options[k] };
-
유틸 타입
Partial
사용하기type OptionsUpdate = Partial<Options>;
-
-
값의 형태를 타입의 형태로 전환하는 방법
const INIT_OPTIONS = { width: 640, height: 480, color: '#00FF00', label: 'VGA', }; type Options = typeof INIT_OPTIONS;
-
함수나 메서드의 반환 값에 명명된 타입 만들기
function getUserInfo(userId: string) { // ... return { userId, name, age, height, weight, favoriteColor }; } // 추론된 반환 타입은 { userId: string; name: string; age: number, ... };
ReturnType
제네릭 사용하기type UserInfo = ReturnType<typeof getUserInfo>;
-
제너릭 타입에서 매개변수 제한하기
interface Name { first: string; last: string; } type DancingDuo<T extends Name> = [T, T]; const couple1: DancingDuo<{ first: string }> = [ { first: 'Kay' }, { first: 'Su' }, ]; // 🚨 에러 // extends를 사용하여 Pick의 정의 완성하기 type Pick<T, K extends keyof T> = { [k in K]: T[k]; }; type FirstLast = Pick<Name, 'first' | 'last'>; // 정상 type FirstMiddle = Pick<Name, 'first' | 'middle'>; // 🚨 에러
아이템 15: 동적 데이터에 인덱스 시그니처 사용하기
-
TS에서는 타입에 ‘인덱스 시그니처’를 명시하여 유연하게 매핑을 표현
// 키의 이름(키의 위치만 표시하는 용도), 키의 타입, 값의 타입 // 🚨 자동완성, 정의로 이동, 이름 바꾸기 등에서 문제 발생 type Rocket = { [property: string]: string }; const rocket: Rocket = { name: 'Falcon 9', variant: 'v1.0', thrust: '4,940 kN', }; // 정상
-
인덱스 시그니처는 부정확하므로 인터페이스 사용
interface Rocket { name: string; variant: string; thrust_kN: number; }
-
인덱스 시그니처는 동적 데이터를 표현할 때 사용
- CSV 파일의 데이터 행을 열 이름과 값으로 매핑하는 객체로 나타내고 싶은 경우, 열 이름이 무엇인지 미리 알 방법이 없을 때 사용
function parseCSV(input: string): { [columnName: string]: string }[] { const lines = input.split('\n'); const [header, ...rows] = lines; const headerColumns = header.split(','); // 연관 배열의 경우, 객체에 인덱스 시그니처를 사용하는 대신, Map 타입을 사용하는 것을 고려 return rows.map((rowStr) => { const row: { [columnName: string]: string } = {}; rowStr.split(',').forEach((cell, i) => { row[headerColumns[i]] = cell; }); return row; }); }
-
특정 타입에 필드가 제한되어 있는 경우 인덱스 시그니처로 모델링 지양
interface Row1 { [column: string]: number; } // 너무 광범위 interface Row2 { a: number; b?: number; c?: number; d?: number; } // 최선 type Row3 = | { a: number } | { a: number; b: number } | { a: number; b: number; c: number } | { a: number; b: number; c: number; d: number }; // 가장 정확하지만 사용하기 번거로움
- Record 사용
type Vec3D = Record<'x' | 'y' | 'z', number>;
- 매핑된 타입 사용(키마다 별도의 타입 사용 가능)
type Vec3D = { [k in 'x' | 'y' | 'z']: number }; type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number };
- Record 사용
아이템 16: Array, 튜플, ArrayLike를 사용하기
number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기
-
JS 객체의 키는 문자열만 가능
- 숫자는 키로 사용 불가
- 배열의 인덱스도 사실은 문자열
-
TS는 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식
- Array의 타입 선언(
lib.es5.d.ts
)interface Array<T> { [n: number]: T; }
- Array의 타입 선언(
-
인덱스 시그니처가
number
로 표현되어 있다면 입력한 값이number
여야 한다는 것을 의미하지만, 실제 런타임에 사용되는 키는string
타입 -
만약 숫자로 인덱싱을 한다면
Array
또는 튜플 타입을 사용하는 것을 권장 -
Array
의 메서드를 사용하고자 하는 게 아니라면ArrayLike
타입을 사용type ArrayLike<T> = { readonly length: number; readonly [n: number | string]: T; }; function checkedAccess<T>(xs: ArrayLike<T>, i: number): T { if (i < xs.length) { return xs[i]; } throw new Error('...'); }
ArrayLike
는 길이와 인덱스 시그니처만 있음ArrayLike
도 키는 숫자 또는 문자열
아이템 17: 변경 오류 방지를 위해 readonly 사용하기
-
함수 파라미터로 넘어가는 배열의 변경을 방지
-
readonly
- 배열의 요소를 읽을 수 있지만, 쓸 수는 없음
length
를 읽을 수 있지만, 바꿀 수는 없음- 배열을 변경하는
pop
을 비롯한 다른 메서드를 호출할 수 없음
-
number[]
는readonly number[]
의 서브타입 -
매개변수를
readonly
로 선언하면?- TS는 매개변수가 함수 내에서 변경이 일어나는지 체크 함
- 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게 됨
- 호출하는 쪽에서 함수에
readonly
배열을 매개변수로 넣을 수도 있음
-
JS에서는 기본적으로 함수가 매개변수를 변경하지 않는다고 가정하지만, 이러한 암묵적인 방법은 타입 체크에 문제를 일으킬 수 있음
-
어떤 함수를
readonly
로 만들면, 그 함수를 호출하는 다른 함수들도 모두readonly
로 만들어야 함(타입의 안전성을 높임) -
readonly
배열을 조작하는 방법arr.length = 0
대신arr = []
arr.push('abc')
대신arr = arr.concat(['abc'])
-
readonly
는 얕게(shallow) 동작한다-
객체로 구성된
readonly
배열이 있다면, 그 객체 자체는 readonly가 아님 -
객체에 사용할 때는
Readonly
제네릭을 사용interface Outer { inner: { x: number; }; } const o: ReadOnly<Outer> = { inner: { x: 0 } }; o.inner = { x: 1 }; // 🚨 에러 o.inner.x = 1; // 정상
-
cf)
ts-essentials
의DeepReadonly
제네릭 -
인덱스 시그니처에
readonly
를 사용하면 객체 속성 변경 방지 가능
-
아이템 18: 매핑된 타입을 사용하여 값을 동기화하기
-
여러번 반복되는 타이핑 줄이기
interface ScatterProps { xs: number[]; ys: number[]; xRange: [number, number]; yRange: [number, number]; color: string; onClick: (x: number, y: number, index: number) => void; } const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = { xs: true, ys: true, xRange: true, yRange: true, color: true, onClick: false, }; function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) { let k: keyof ScatterProps; for (k in oldProps) { if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) { return true; } } return false; }
- 매핑된 타입을 사용해서 관련된 값과 타입을 동기화할 수 있음
- 인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려 해야 함
✅ 3장 타입 추론
아이템 19: 장황한 코드 방지하기
-
코드의 모든 변수에 타입을 선언하는 것은 비생산적
-
객체는 비구조화 할당문 사용 지향
-
모든 지역 변수의 타입이 추론되도록 해야 함
function logProduct(product: Product) { const { id, name, price } = product; console.log(id, name, price); // 타입 Product로 추론되어야 함 }
-
-
타입 구문을 생략하는 경우
- 함수 내에서 생성된 지역 변수
- 함수 파라미터에 기본 값이 있는 경우
-
타입을 명시하면 좋은 경우
-
객체 리터럴을 정의할 때, 잉여 속성 체크가 동작 함
-
함수의 반환 타입
-
함수의 입출력 타입에 대해 더욱 명확하게 알 수 있음
-
명명된 타입을 사용할 수 있음
interface Vector2D { x: number; y: number; } // 이 함수의 반환 타입은 Vector2D 와 호환되지 않음 function add(a: Vector2D, b: Vector2D) { return { x: a.x + b.x, y: a.y + b.y }; }
-
-
-
cf) eslint 규칙 중
no-inferrable-types
사용 가능- 작성된 모든 타입 구문이 정말로 필요한지 확인
아이템 20: 다른 타입에는 다른 변수 사용하기
-
변수의 값은 바뀔 수 있지만, 그 타입은 바뀌지 않음
-
타입 확장하기 - 유니온 타입
let id: string | number = '12-34-56'; // 개선 - let 대신 const 사용 const newId = '12-34-56'; const serial = 123456;
아이템 21: 타입 넓히기
-
TS가 작성된 코드를 체크하는 정적 분석 시점에, 변수는
가능한
값들의 집합인 타입을 가짐 -
TS의 타입
넓히기
- 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추하는 것
// 변수 x는 할당 시점에 넓히기가 동작해서 string으로 추론 됨 // const 사용 지향 let x = 'x';
- 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추하는 것
-
넓히기를 제어하는 방법
-
const
로 변수 선언 -
객체에서 TS의 넓히기 알고리즘은 각 요소를
let
으로 할당된 것처럼 다룸const v = { x: 1 }; v.x = 3; // 정상 v.x = '3'; // 🚨 '3' 형식은 'number' 형식에 할당할 수 없음 v.y = 4; // 🚨 '{ x: number; }' 형식에 'y' 속성이 없음 v.name = 'Kay'; // 🚨 '{ x: number; }' 형식에 'name' 속성이 없음
-
-
TS의 기본 동작 재정의
- 명시적 타입 구문 제공
const v: { x: 1 | 3 | 5 } = { x: 1, }; // 타입이 { x: 1 | 3 | 5; }
- 명시적 타입 구문 제공
-
타입 체커에 추가적인 문맥 제공 ex) 함수의 매개변수로 값을 전달
-
const
단언문 사용하기 (as const
)const v1 = { x: 1, y: 2, } // 타입은 { x: number, y: number; } const v2 = { x: 1 as const; y: 2, }; // 타입은 { x: 1, y: number; } const v3 = { x: 1, y: 2, } as const; // 타입은 { readonly x: 1; readonly y: 2; }
아이템 22: 타입 좁히기
-
분기문에서 예외를 던지거나, 함수를 반환하여 블록의 나머지 부분에서 변수의 타입 좁히기
-
instanceof
으로 타입 좁히기 -
속성 체크로 타입 좁히기
interface A { a: number; } interface B { b: number; } function pickAB(ab: A | B) { if ('a' in ab) { ab; // 타입이 A } else { ab; // 타입이 B } ab; // 타입이 a | B }
-
Array.isArray
등의 내장 함수로 타입 좁히기 -
null
체크 시typeof null === 'object'
가 됨 -
명시적
태그
붙이기 (tagged union
)function handleEvent(e: AppEvent) { switch (e.type) { case 'download': e; break; case 'upload': e; break; } }
-
TS를 돕기 위해 커스텀 함수 도입(사용자 정의 타입 가드)
function isInputElement(el: HTMLElement): el is HTMLInputElement { return 'value' in el; }
-
배열에서
undefined
걸러내기function isDefined<T>(x: T | undefined): x is T { return x !== undefined; } const members = ['Janet', 'Michael'] .map((who) => jackson5.find((n) => n === who)) .filter(isDefined); // 타입이 string[]
아이템 23: 한꺼번에 객체 생성하기
-
TS의 타입은 일반적으로 변경되지 않음. 따라서 객체를 생성할 때는 속성을 하나씩 추가하기보다는 여러 속성을 포함해서 한꺼번에 생성해야 타입 추론에 유리
-
객체를 제 각각 나눠야 한다면, 타입 단언문(
as
)을 사용interface Point { x: number; y: number; } const pt = {} as Point; pt.x = 3; pt.y = 4; // 정상
- 객체 전개 연산자(
...
) 사용
- 객체 전개 연산자(
-
선택적 필드 방식으로 표현
function addOptional<T extends object, U extends object>( a: T, b: U | null ): T & Partial<U> { return { ...a, ...b }; } const nameTitle = { name: 'Kay', title: 'Dev' }; const ko = addOptional( nameTitle, hasDates ? { start: -1589, end: -1566 } : null );
아이템 24: 일관성 있는 별칭 사용하기
-
별칭을 남발하면 제어 흐름을 분석하기 어려움
-
객체의 속성을 별칭에 할당하면
strictNullChecks
에서 걸릴 위험이 있음interface Polygon { exterior: Coordinate[]; holes: Coordinate[][]; box?: BoundingBox; } // 속성 체크는 polygon.box의 타입을 정제했지만 box는 그렇지 않음 function isPointInPolygon(polygon: Polygon, pt: Coordinate) { polygon.box; // 타입이 BoundingBox | undefined const box = polygon.box; box; // 타입이 BoundingBox | undefined if (polygon.box) { polygon.box; // 타입이 BoundingBox box; // 타입이 BoundingBox | undefined } } // 객체 비 구조화 할당 이용 function isPointInPolygon(polygon: Polygon, pt: Coordinate) { const { box } = polygon; if (box) { const { x, y } = box; // ... } } // 객체 비구조화 이용 시 주의사항 // - 전체 box 속성이 아니라 x와 y가 선택적 속성일 경우 속성 체크가 더 필요 함 // - box에는 선택적 속성이 적합했지만 holes에는 그렇지 않음 // - 런타임에도 혼동을 야기할 가능성 // - 속성보다 지역 변수 사용
아이템 25: 비동기 코드는 콜백 대신 async 함수 사용
-
과거 JS의 비동기 콜백 지옥 발생
- ES2015는
Promise
개념을 도입 - ES2017에서는
async/await
도입 - TS 런타임에 관계없이
async/await
사용 가능 - TS의 프로미스 반환 타입은
Promise<Response>
- ES2015는
-
일반적으로
Promise
보다는async/await
을 권장- 더 간결하고 직관적
async
함수는 항상 프로미스를 반환하도록 강제 됨
// function getNumber(): Promise<number> async function getNumber() { return 42; }
-
콜백이나 프로미스를 사용하면 실수로 반(half)동기 코드를 작성할 수 있지만,
async
를 사용하면 항상 비동기 코드를 작성할 수 있음const _cache: { [url: string]: string } = {}; async function fetchWithCache(url: string) { if (url in _cache) { return _cache[url]; } const response = await fetch(url); const text = await response.text(); _cache[url] = text; return text; } let requestStatus: 'loading' | 'success' | 'error'; async function getUser(userId: string) { requestStatus = 'loading'; const profile = await fetchWithCache(`/user/${userId}`); requestStatus = 'success'; }
-
async
함수에서 프로미스를 반환하면 반환 타입은Promise<Promise<T>>
가 아닌,Promise<T>
가 됨// function getJSON(url: string): Promise<any> async function getJSON(url: string) { const response = await fetch(url); const jsonPromise = response.json(); // 타입이 Promise<any> return jsonPromise; }
아이템 26: 타입 추론에 문맥이 어떻게 사용되는지 이해하기
-
문자열 타입을 문자열 리터럴 타입의 유니온으로 사용하는 경우
type Language = 'JavaScript' | 'TypeScript' | 'Python'; function setLanguage(language: Language) { /* ... */ } setLanguage('JavaScript'); // 정상 let language = 'JavaScript'; setLanguage(language); // 🚨 에러 language는 string type
- 해결 방법
- 타입 선언에서 language의 가능한 값을 제한
let language: Language = 'JavaScript'; setLanguage(language); // 정상
const
를 사용하여 타입 체커에게 변경할 수 없다고 할 수 있음
-
튜플 사용 시 주의점
- 위와 마찬가지로 값을 분리 당함
function panTo(where: [number, number]) { /* ... */ } panTo([10, 20]); // 정상 const loc = [10, 20]; // 🚨 'number[]' 형식의 인수는 '[number, number]' 형식의 매개변수에 할당될 수 없음 panTo(loc);
-
해결 방법
-
타입 선언 제공
const loc: [number, number] = [10, 20]; panTo(loc); // 정상
-
상수 문맥 제공
const loc = [10, 20] as const; // 🚨 에러: 'readonly [10, 20]' 형식은 '[number, number]'에 할당할 수 없음 panTo(loc);
-
최선의 해결책
function panTo(where: readonly [number, number]) { /* ... */ } const loc = [10, 20] as const; panTo(loc); // 정상
-
-
객체 사용 시 주의점
- 문자열 리터럴이나 튜플을 포함하는 큰 객체에서 상수를 뽑아낼 때, 프로퍼티 타입이
string
으로 추론되는 경우 타입 단언이나 상수 단언을 사용할 수 있음
- 문자열 리터럴이나 튜플을 포함하는 큰 객체에서 상수를 뽑아낼 때, 프로퍼티 타입이
-
콜백 사용 시 주의점
- 콜백을 다른 함수로 전달할 때, TS는 콜백의 매개변수 타입을 추론하기 위해 문맥을 사용. 이 경우 넘겨주는 함수의 매개변수에 타입 구문을 추가해서 해결할 수 있음.
아이템 27: 함수형 기법과 라이브러리로 타입 흐름 유지
-
함수형 프로그래밍을 지원하는 최근의 라이브러리
- ex)
map, flatMap, filter, reduce
등 - 타입 정보가 그대로 유지되면서 타입 흐름(flow)이 계속 전달 됨
- ex)
-
lodash의
Dictionary
타입// 타입이 _.Dictionary<string>[] const rows = rawRows .slice(1) .map((rowStr) => _.zipObject(headers, rowStr.split(',')));
Dictionary<string>
은{[key: string]: string}
또는Record<string, string>
과 동일
-
flat
메서드T[][] => T[]
declare const rosters: { [team: string]: BasketBallPlayer[] }; // 타입이 BasketBallPlayer[] const allPlayers = Object.values(rosters).flat();
-
TS의 많은 부분이 JS 라이브러리의 동작을 정확히 모델링하기 위해서 개발되었으므로, 라이브러리 사용 시 타입 정보가 잘 유지되는 점을 활용
아이템 28: 유효한 상태만 표현하는 타입을 지향하기
-
애플리케이션의 상태 표현하기
interface RequestPending { state: 'pending'; } interface RequestError { state: 'error'; error: string; } interface RequestSuccess { state: 'ok'; pageText: string; } type RequestState = RequestPending | RequestError | RequestSuccess; interface State { currentPage: string; requests: { [page: string]: RequestState }; }
- 모든 상황 고려하기
- 어떤 값들을 포함하고 어떤 값들을 제외할지 신중하기 생각하기
아이템 29: 사용할 때는 너그럽게, 생성할 때는 엄격하게
-
TCP 구현체의 견고성 원칙 또는 포스텔의 법칙(함수의 시그니처에도 적용가능)
- 함수의 매개변수는 타입의 범위가 넓어도 되지만, 결과를 반환할 때는 일반적으로 타입의 범위가 더 구체적이어야 함
-
예시
- 👎 Bad Case
declare function setCamera(camera: CameraOptions): void; declare function viewportForBounds(bounds: LngLatBounds): CameraOptions; interface CameraOptions { center?: LngLat; zoom?: number; bearing?: number; pitch?: number; } type LngLat = | { lng: number; lat: number } | { lon: number; lat: number } | [number, number];
- 👍 Good Case
interface LngLat { lng: number; lat: number; } type LngLatLike = LngLat | { lon: number; lat: number } | [number, number]; interface Camera { center: LngLat; zoom: number; bearing: number; pitch: number; } interface CameraOptions extends Omit<Partial<Camera>, 'center'> { center?: LngLatLike; } type LngLatBounds = | { northeast: LngLatLike; southwest: LngLatLike } | [LngLatLike, LngLatLike] | [number, number, number, number]; declare function setCamera(camera: CameraOptions): void; declare function viewportForBounds(bounds: LngLatBounds): Camera;
→ 매개변수와 반환 타입의 재사용을 위해서 기본 형태(반환 타입)와 느슨한 형태(매개변수 타입)를 지향
아이템 30: 문서에 타입 정보를 쓰지 않기
- 타입 구문은 TS 타입 체커가 타입 정보를 동기화하도록 강제
- 함수의 입력과 출력의 타입을 코드로 표현하는 것이 주석보다 더 나음
- 값을 변경하지 않는다고 설명하는 주석 대신,
readonly
사용 - 변수명에 타입 정보 넣지 않기 (단위가 있는 숫자들은 제외)
아이템 31: 타입 주변에 null 값 배치하기
- 문제가 있는 예제
// 최솟값이나 최댓값이 0인 경우
// numbs 배열이 비어있는 경우
function extent(numbs: number[]) {
let min, max;
for (const num of numbs) {
if (!min) {
min = num;
max = num;
} else {
min = Math.min(min, num);
max = Math.max(max, num);
}
}
return [min, max];
}
- min과 max를 한 객체 안에 넣고
null
이거나null
이 아니게 하기
function extent(numbs: number[]) {
let result: [number, number] | null = null;
for (const num of numbs) {
if (!result) {
result = [num, num];
} else {
result = [Math.min(num, result[0]), Math.max(num, result[1])];
}
}
return [min, max];
}
null
과null
이 아닌 값을 섞어서 클래스 만들기
class userPosts {
user: UserInfo;
posts: Post[];
constructor(user: UserInfo, posts: Post[]) {
this.user = user;
this.posts = posts;
}
static async init(userId: string): Promise<UserPosts> {
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchPostsForUser(userId),
]);
return new UserPosts(user, posts);
}
getUserName() {
return this.user.name;
}
}
- 정리
- 값들 중
null
여부에 따라, 다른 값이 암시적으로null
이 될 수있는 가능성을 두고 설계하면 안 됨 - API 작성 시에는 반환 타입을 큰 객체로 만들고, 반환 타입 전체가
null
이거나null
이 아니게 만들어야 함 - 클래스를 만들 때는 필요한 모든 값이 준비되었을 때, 생성하여
null
이 존재하지 않도록 하는 것이 좋음
- 값들 중
아이템 32: 인터페이스의 유니온을 사용하기
유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
-
문제가 있는 예제
interface Layer { layout: FillLayout | LineLayout | PointLayout; paint: FillPaint | LinePaint | PointPaint; }
→ 각각 타입의 계층을
분리된 인터페이스
로 나누기interface FillLayer { type: 'fill'; layout: FillLayout; paint: FillPaint; } interface LineLayer { type: 'line'; layout: LineLayout; paint: LinePaint; } interface PointLayer { type: 'point'; layout: PointLayout; paint: PointPaint; } type Layer = FillLayer | LineLayer | PointLayer;
-
태그드 유니온
사용(TS는 태그를 참고하여 범위를 좁힐 수 있음)function drawLayer(layer: Layer) { if (layer.type === 'fill') { const { paint } = layer; // 타입이 FillPaint const { layout } = layer; // 타입이 FillLayout } else // ...
-
여러 개의 선택적 필드가 동시에 값이 있거나 동시에
undefined
인 경우, 두 개의 속성을 하나의 객체로 모음interface Person { name: string; // birthPlace와 birthDate를 하나로 모음 birth?: { place: string; date: Date; }; }
아이템 33: string 타입보다 더 구체적인 타입 사용하기
-
좋지 못한 예시
interface Album { artist: string; title: string; releaseDate: string; recordingType: string; }
-
타입을 제한하거나, 유니온 타입을 사용하
type RecordingType = 'studio' | 'live'; interface Album { artist: string; title: string; releaseDate: Date; recordingType: RecordingType; }
-
함수의 매개변수에
string
을 잘못 사용하지 않도록 주의// 🚨 '{}' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any' 형식이 있음 function pluck(records: any[], key: string): any[] { return records.map((r) => r[key]); }
-
제네릭과
keyof
을 사용type K = keyof Album; // 이때 TS는 반환 타입을 추론함 function pluck<T>(records: T[], key: keyof T) { return records.map((r) => r[key]); }
-
keyof
T로 범위 더 좁힐 수 있음function pluck<T, K extends keyof T>(records: T[], key: K): T[K][] { return records.map((r) => r[key]); }
-
결과
pluck(albums, 'releaseDate'); // 타입이 Date[] pluck(albums, 'artist'); // 타입이 string[] pluck(albums, 'recordingType'); // 타입이 RecordingType[]
아이템 34: 부정확한 타입보다는 미완성 타입 사용하기
-
코드를 더 정밀하게 만들어서, 코드가 오히려 더 부정확해지는 문제
interface Point { type: 'Point'; coordinates: number[]; } interface LineString { type: 'LineString'; coordinates: number[][]; } interface Polygon { type: 'Polygon'; coordinates: number[][]; } type Geometry = Point | LineString | Polygon; // 다른 것들도 추가될 수 있다
-
아래와 같이 구체화하는 경우
GeoPosition
위치정보에는 추가 정보가 들어갈 수 없게 됨type GeoPosition = [number, number]; interface Point { type: 'Point'; coordinates: GeoPosition; }
-
부정확함을 바로잡는 방법을 쓰는 대신, 테스트 세트를 추가하여 놓친 부분이 없는지 확인
type CallExpression = MathCall | CaseCall | RGBCall; type Expression = number | string | CallExpression; interface MathCall { 0: '+' | '-' | '/' | '*' | '>' | '<'; 1: Expression; 2: Expression; length: 3; } interface CaseCall { 0: 'case'; 1: Expression; 2: Expression; 3: Expression; length: 4 | 6 | 8 | 10 | 12 | 14 | 16; // 등등 } interface RGBCall { 0: 'rgb'; 1: Expression; 2: Expression; 3: Expression; length: 4; }
→ 잘못 사용된 코드에서 오류가 발생하기는 하지만, 오류 메시지가 더 난해 해짐
-
타입이 구체적으로 정제된다고 해서 정확도가 무조건 올라가지는 않음
-
현 상황을 고려하면서 타입 설계
아이템 35: API와 명세를 보고 타입 만들기
- 명세를 기반으로 타입을 작성한다면, 사용 가능한 모든 값에 대해서 코드가 작동한다는 확신을 가질 수 있음
아이템 36: 해당 분야의 용어로 타입 이름 짓기
- 동일한 의미를 나타낼 때는 같은 용어를 사용
data, info, thing, item, object, entity
같은 모호하고 의미없는 이름 지양- 네이밍 할 때, 포함된 내용이나 계산 방식이 아니라, 데이터 자체가 무엇인지를 고려
아이템 37: 공식 명칭에는 상표를 붙이기
-
공식 명칭 (nominal typing)
- 타입이 아니라, 값의 관점
interface Vector2D { x: number; y: number; _brand: '2d'; } function vec2D(x: number, y: number): Vector2D { return { x, y, _brand: '2d' }; } function calculateNorm(p: Vector2D) { return Math.sqrt(p.x * p.x + p.y * p.y); } calculateNorm(vec2D(3, 4)); // 정상 const vec3D = { x: 3, y: 4, z: 1 }; calculateNorm(vec3D); // 🚨 '_brand' 속성이 ... 형식에 없습니다
-
상표 시스템은 타입 시스템에서 동작하지만, 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있음
-
TS는 구조적 타이핑(덕 타이핑)을 사용하기 때문에, 값을 구분하기 위해 공식 명칭이 필요할 경우 상표를 붙일 수 있음
아이템 38: any 타입은 한 좁은 범위에서만 사용하기
-
any
작성 방식function f1() { const x: any = expressionReturningFoo(); // X processBar(x); } function f2() { const x = expressionReturningFoo(); // O processBar(x as any); }
any
타입이processBar
함수의 매개변수에만 사용된 표현식이므로 다른 코드에는 영향을 미치지 않기 때문
-
TS가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋음
-
강제로 타입 오류 제거 시
any
대신@ts-ignore
사용// 근본적인 문제 해결은 아님 function f1() { const x = expressionReturningFoo(); // @ts-ignore processBar(x); return x; }
-
객체와 관련한
any
의 사용법// 모든 속성이 타입 체크가 되지 않는 부작용 발생 const config: Config = { a: 1, b: 2, c: { key: value, }, } as any; // X const config: Config = { a: 1, b: 2, // 이 속성은 여전히 체크됨 c: { key: value as any, }, };
아이템 39: any를 구체적으로 변형해서 사용하기
-
일반적인 상황에서는
any
보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높음function getLengthBad(array: any) { // X return array.length; } function getLength(array: any[]) { return array.length; }
-
함수 매개변수로 객체 사용 시 타입 구체화
function hasTwelveLetterKey(o: { [key: string]: any }) { for (const key in o) { if (key.length === 12) { return true; } } return false; }
-
함수 타입 구체화
type Fn0 = () => string; // 매개변수 없이 호출 가능한 모든 함수 type Fn1 = (arg: string[]) => string[]; // 매개변수 1개 type FnN = (...args: string[]) => string[]; // 모든 개수의 매개변수 ("Function" 타입과 동일)
아이템 40: 함수 안으로 타입 단언문 감추기
- 함수 내부에는 타입 단언 사용하고, 함수 외부로 드러나는 타입은 정의를 정확히 명시하는 것이 좋음
-
어떤 함수든 캐싱할 수 있는 래퍼 함수
cacheWrapper
declare function cacheWrapper<T extends Function>(fn: T): T; declare function shallowEqual(a: any, b: any): boolean; // TS는 반환문에 있는 함수와 원본 함수 T 타입이 어떤 관련이 있는지 알지 못하기 때문에 오류 발생 function cacheWrapper<T extends Function>(fn: T): T { let lastArgs: any[] | null = null; let lastResult: any; return function (...args: any[]) { // 🚨 '(...args: any[]) => any' 형식은 'T' 형식에 할당할 수 없습니다. if (!lastArgs || !shallowEqual(lastArgs, args)) { lastResult = fn(...args); lastArgs = args; } return lastResult; }; }
-
단언문을 추가해서 오류를 제거
function cacheWrapper<T extends Function>(fn: T): T { let lastArgs: any[] | null = null; let lastResult: any; return function (...args: any[]) { if (!lastArgs || !shallowEqual(lastArgs, args)) { lastResult = fn(...args); lastArgs = args; } return lastResult; } as unknown as T; }
-
객체를 매개변수로 하는
shallowObjectEqual
declare function shallowObjectEqual<T extends object>(a: T, b: T): boolean; function shallowObjectEqual<T extends object>(a: T, b: T): boolean { for (const [(k, value)] of Object.entries(a)) { if (!(k in b) || value !== (b as any)[k]) { // b[k] 구문에 타입 단언 필요 return false; } } return Object.keys(a).length === Object.keys(b).length; }
아이템 41: Any 타입의 변환
-
예제 코드
// out의 타입은 any[]로 선언되었지만, // number 타입의 값을 넣는 순간부터 타입은 number[]로 변환 function range(start: number, limit: number) { const out = []; // 타입이 any[] for (let i = start; i < limit; i++) { out.push(i); // out의 타입이 any[] } return out; // 타입이 number[] }
-
타입의 전환
-
배열에 다양한 타입의 요소를 넣으면 배열의 타입이 변환됨
const result = []; // 타입 any[] result.push('a'); // 타입 string[] result.push(1); result; // 타입 (string | number)[]
-
기타
- 조건문에서는 분기에 따라 타입이 변환
- 변수의 초깃값이
null
인 경우도any
의 변환 발생
-
any
타입의 변환은noImplicitAny
가 설정된 상태에서 변수의 타입이 암시적any
인 경우에만 발생한며, 명시적any
선언 시 타입이 그대로 유지됨 -
any
타입의 변환은 암시적any
타입에 어떤 값을 할당할 때만 발생하며, 암시적any
타입은 함수 호출을 거쳐도 변환되지 않음 -
타입을 안전하게 지키기 위해서는 암시적
any
를 진화시키는 방식보다, 명시적 타입 구문을 사용하는 것이 좋음
아이템 42: any 대신 unknown 사용하기
-
함수의 반환값에
unknown
사용function parseYAML(yaml: string): any { // ... } function safeParseYAML(yaml: string): unknown { return parseYAML(yaml); } const book = safeParseYAML(` name: Kay author: Charlotte Bronte `) as Book; alert(book.title); // 🚨 'Book' 형식에 'title' 속성이 없습니다. book('read'); // 🚨 이 식은 호출할 수 없습니다.
-
any
가 강력하면서도 위험한 이유- 어떠한 타입이든
any
타입에 할당 가능 - 어떠한 타입이든
unknown
타입에 할당 가능 - 어떠한 타입도
never
에 할당할 수 없음 any
타입은 어떠한 타입으로도 할당 가능unknown
은 오직unknown
과any
에만 할당 가능never
타입은 어떠한 타입으로도 할당 가능 → 타입 시스템과 상충됨
- 어떠한 타입이든
-
instanceof
체크 후unknown
에서 원하는 타입으로 변환function processValue(val: unknown) { if (val instanceof Date) { val; // 타입이 Date } }
-
사용자 정의 타입 가드로
unknown
에서 원하는 타입으로 변환function isBook(val: unknown): val is Book { return ( typeof val === 'object' && val !== null && 'name' in val && 'author' in val ); } function processValue(val: unknown) { if (isBook(val)) { val; // 타입이 Book } }
-
unknown
대신 제네릭 매개변수 사용// 타입 단언문과 똑같음 // 제네릭보다는 unknown을 반환하고, 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋음 function safeParseYAML<T>(yaml: string): T { return parseYAML(yaml); }
-
단언문
declare const foo: Foo; let barAny = foo as any as Bar; let barUnk = foo as unknown as Bar;
unknown
의 경우 분리되는 즉시 오류를 발생하므로any
보다 안전(에러가 전파되지 않음)
-
정말
null
과undefined
가 불가능하다면,unknown
대신{}
사용
아이템 43: 몽키 패치보다는 안전한 타입을 사용하기
-
JS는 객체나 클래스에 임의의 속성을 추가할 수 있음
window.monkey = 'Tamarin'; document.monkey = 'Howler'; // 'Document' 유형에 'monkey' 속성이 없습니다 document.monkey = 'Tamarin'; // 해결 // 단 타입 안정성을 해치는 안 좋은 코드 (document as any).monkey = 'Tamarin'; // 정상
- 일반적으로 좋은 설계는 아님(전역 변수 사이드 이펙트의 문제)
-
interface
의 보강(augmentation)-
보강은 전역적으로 적용되기 때문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없음
interface Document { monkey: string; } document.monkey = 'Tamarin'; // 정상 // 모듈 관점에서라면 global 선언 추가 export {}; declare global { interface Document { monkey: string; } } document.monkey = 'Tamarin'; // 정상
-
-
더 구체적인 타입 단언문 사용
interface MonkeyDocument extends Document { monkey: string; } (document as MonkeyDocument).monkey = 'Macaque'; // 정상
아이템 44: 타입 커버리지 추적해 타입 안전성 유지
any
타입이 여전히 프로그램 내에 존재할 수 있는 2가지 경우
-
명시적
any
타입 ex)any[], {[key: string]: any}
-
서드파티 타입 선언
-
@types
선언 파일로부터any
타입이 전파되는 경우 -
가장 극단적인 예시는 전체 모듈에
any
타입을 부여하는 경우// my-module 에서 어떤 것이든 오류 없이 임포트할 수 있음 declare module 'my-module';
-
타입에 버그가 있는 경우 : 선언된 타입과 실제 반환된 타입이 맞지 않는 경우
-
- npm의
type-coverage
패키지 활용하여any
추적하기
아이템 45: devDependencies에 TS,@types 추가
-
npm
의 의존성 구분dependencies
: 현재 프로젝트 실행 시 필수적인 라이브러리devDependencies
: 런타임에는 필요없는 라이브러리peerDependencies
: 런타임에 필요하긴 하지만, 의존성을 직접 관리하지 않는 라이브러리
-
TS는 개발 도구일 뿐이고 타입 정보는 런타임에 존재하지 않기 때문에, TS와 관련된 라이브러리는 일반적으로
devDependencies
에 속함 -
TS 프로젝트에서 고려해야 할 의존성
- TS 시스템 레벨로 설치하기보다는
devDependencies
에 넣는 것을 권장npm install
시 팀원들 모두 항상 정확한 버전의 TS 설치 가능
- 대부분의 TS IDE와 빌드 도구는
devDependencies를
통해 설치된 타입스크립트의 버전을 인식할 수 있음 DefinitelyTyped
에서 라이브러리에 대한 타입 정보를 얻을 수 있음@types
라이브러리는 타입 정보만 포함하고 있으며 구현체는 포함하지 않음- 원본 라이브러리 자체가
dependencies
에 있더라도@types
의존성은devDependencies
에 있어야 함
- TS 시스템 레벨로 설치하기보다는
아이템 46: 타입 선언과 관련된 3가지 버전 이해하기
-
TS 사용 시 고려해야 할 사항
- 라이브러리의 버전
- 타입 선언(@types)의 버전
- TS의 버전
-
TS에서 의존성을 사용하는 방식
- 특정 라이브러리는
dependencies
로, 타입 정보는devDependencies
로 설치
- 특정 라이브러리는
-
실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점
-
라이브러리를 업데이트했지만 실수로 타입 선언은 업데이트하지 않은 경우
- 타입 선언도 업데이트하여 라이브러리와 버전을 맞춤
- 보강 기법 또는 타입 선언의 업데이트를 직접 작성
-
라이브러리보다 타입 선언의 버전이 최신인 경우
- 라이브러리 버전을 올리거나 타입 선언의 버전을 내리기
-
프로젝트에서 사용하는 TS 버전보다 라이브러리에서 필요로 하는 타입스크립트 버전이 최신인 경우
- TS의 최신 버전을 사용
- 라이브러리 타입 선언의 버전을 내리거나,
declare module
선언으로 라이브러리의 타입 정보를 없애 버림
-
@types
의존성이 중복되는 경우-
ex)
@types/bar
가 현재 호환되지 않는 버전의@types/foo
에 의존하는 경우 -
전역 네임스페이스에 있는 타입 선언 모듈인 경우 중복 문제가 발생 → 서로 버전이 호환되도록 업데이트
-
일부 라이브러리는 자체적으로 타입 선언을 포함(번들링)
-
package.json
의types
필드가.d.ts
파일을 가리키도록 되어 있음 -
버전 불일치 문제를 해결할 수 있지만, 네 가지 부수적인 문제점이 있음
- 번들된 타입 선언에 보강 기법으로 해결할 수 없는 오류가 있는 경우, 또는 공개 시점에는 잘 동작했지만 TS 버전이 올라가면서 오류가 발생하는 경우(번들된 타입에서는
@types
의 버전 선택 불가능) - 프로젝트 내의 타입 선언이 다른 라이브러리의 타입 선언에 의존하는 경우(
devDependencies
에 들어간 의존성을 다른 사용자는 설치할 수 없기 때문) →DefinitelyTyped
에 타입 선언을 공개하여 타입 선언을@types
로 분리 - 프로젝트의 과거 버전에 있는 타입 선언에 문제가 있는 경우 → 과거 버전으로 돌아가서 패치 업데이트를 함
- 타입 선언의 패치 업데이트를 자주 하기 어렵다는 문제
- 번들된 타입 선언에 보강 기법으로 해결할 수 없는 오류가 있는 경우, 또는 공개 시점에는 잘 동작했지만 TS 버전이 올라가면서 오류가 발생하는 경우(번들된 타입에서는
-
-
-
잘 작성된 타입 선언은 라이브러리를 올바르게 사용하는 방법에 도움이 되며 생산성을 크게 향상시킴
-
라이브러리 공개 시, 타입 선언을 자체적으로 포함하는 것과 타입 정보만 분리하여
DefinitelyTyped
에 공개하는 것의 장단점을 비교 해야 함 -
라이브러리가 타입스크립트로 작성된 경우만 타입 선언을 라이브러리에 포함하는 것을 권장
아이템 47: Public API 모든 타입 Export
-
라이브러리 제작자는 프로젝트 초기에 타입 Export 부터 작성해야 함
-
타입을 Export 하지 않았을 경우
// 해당 라이브러리 사용자는 SecretName 또는 SecretSanta 를 직접 import 할 수 없고, getGift만 import 할 수 있음 interface SecretName { first: string; last: string; } interface SecretSanta { name: SecretName; gift: string; } export function getGift(name: SecretName, gift: string): SecretSanta { // ... }
-
Parameters
와ReturnType
을 이용해 추출type MySanta = ReturnType<typeof getGift>; // SecretSanta type MyName = Parameters<typeof getGift>[0]; // SecretName
→ 사용자가 추출하기 전에 공개 메서드에 사용된 타입은 Export 지향
-
아이템 48: API 주석에 TSDoc 사용하기
-
함수 주석에
// ...
대신 JSDoc 스타일의/** ... **/
을 사용하면 대부분의 편집기는 함수 사용부에서 주석을 툴팁으로 표시해 줌 -
타입스크립트 관점의 TSDoc
/** * Generate a greeting * @param name Name of the person to greet * @param title ... * returns ... */ function greetFullTSDoc(name: string, title: string) { return `Hello ${title} ${name}`; }
-
타입 정의에 TSDoc 사용하기
/** 특정 시간과 장소에서 수행된 측정 */ interface Measurement { /** 어디에서 측정되었나? */ position: Vector3D; /** 언제 측정되었나? */ time: number; /** 측정된 운동량 */ momentum: Vector3D; } // Measurement 객체의 각 필드에 마우스를 올려 보면 필드별로 설명을 볼 수 있음
-
TS에서는 타입 정보가 코드에 있기 때문에 TSDoc에서는 타입 정보를 명시하면 안 됨(주의)
아이템 49: 콜백에서 this에 대한 타입 제공하기
-
JS에서
this
는 다이나믹 스코프정의된
방식이 아니라호출된
방식에 따라 달라짐
-
TS는 JS의
this
바인딩을 그대로 모델링 함 -
this
를 사용하는 콜백 함수에서this
바인딩 문제 해결-
콜백 함수의 매개변수에
this
를 추가하고, 콜백 함수를call
로 호출하는 방법// 이때 반드시 call 을 사용해야 함 function addKeyListener( el: HTMLElement, fn: (this: HTMLElement, e: KeyboardEvent) => void ) { el.addEventListener('keydown', (e) => { fn.call(el, e); }); }
-
만약 라이브러리 사용자가 콜백을 화살표 함수로 작성하고
this
를 참조하려고 하면 TS가 문제를 잡아 냄class Foo { registerHandler(el: HTMLElement) { addKeyListener(el, (e) => { this.innerHTML; // 'Foo' 유형에 'innerHTML' 속성이 없음 }); } }
-
-
콜백 함수에서
this
값을 사용해야 한다면,this
는 API의 일부가 되는 것이기 때문에 반드시 타입 선언에 포함해야 함
아이템 50: 오버로딩 타입보다는 조건부 타입 사용
-
두 가지 타입의 매개변수를 받는 함수
// 선언문에는 number 타입을 매개변수로 넣고 string 타입을 반환하는 경우도 포함되어 있음 function double(x: number | string): number | string; function double(x: any) { return x + x; } const num = double(12); // string | number const str = double('x'); // string | number
→ 제네릭을 사용하여 동작을 모델링할 수 있음
// 타입이 너무 과하게 구체적인 문제 function double<T extends number | string>(x: T): T; function double(x: any) { return x + x; } const num = double(12); // 타입이 12 const str = double('x'); // 타입이 'x' (😮 string을 원하고 있다.)
-
조건부 타입
-
타입 공간의
if
구문function double<T extends number | string>( x: T ): T extends string ? string : number; function double(x: any) { return x + x; }
-
개별 타입의 유니온으로 일반화하기 때문에 타입이 더 정확해짐
-
각각이 독립적으로 처리되는 타입 오버로딩과 달리, 조건부 타입은 타입 체커가 단일 표현식으로 받아들이기 때문에 유니온 문제를 해결할 수 있음
아이템 51: 의존성 분리를 위해 미러 타입 사용
-
CSV 파일을 파싱하는 라이브러리 작성 시, NodeJS 사용자를 위해 매개변수에
Buffer
타입을 허용하는 경우Buffer
타입 정의를 위해@types/node
패키지 필요- 그러나 다른 라이브러리 사용자들은 해당 패키지가 불필요
-
각자가 필요한 모듈만 사용할 수 있도록 구조적 타이핑 적용
// CsvBuffer가 Buffer 타입과 호환되기 때문에 NodeJS 프로젝트에서도 사용 가능 interface CsvBuffer { toString(encoding: string): string; } function parseCSV( contents: string | CsvBuffer ): { [column: string]: string }[] { // ... } parseCSV(new Buffer('column1, column2\nval2,val2', 'utf-8'));
-
미러링
- 작성 중인 라이브러리가 의존하는 라이브러리의 구현과 무관하게 타입에만 의존한다면, 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것
-
다른 라이브러리의 타입이 아닌 구현에 의존하는 경우에도 동일한 기법을 적용할 수 있고 타입 의존성을 피할 수 있음
→ 유닛 테스트와 상용 시스템 간의 의존성을 분리하는 데도 유용
아이템 52: 테스팅 타입의 함정에 주의하기
-
타입 선언 테스트
- 유틸리티 라이브러리에서 제공하는
map
함수의 타입 작성
// 단순히 함수를 호출하는 테스트만으로는 반환값에 대한 체크가 누락될 수 있음 (’실행’에서의 오류만 검사함) declare function map<U, V>(array: U[], fn: (u: U) => V): V[];
- 유틸리티 라이브러리에서 제공하는
-
반환값을 특정 타입의 변수에 할당하여 간단히 반환 타입을 체크할 수 있는 방법
// number[] 타입 선언은 map 함수의 반환 타입이 number[] 임을 보장 const lengths: number[] = map(['john', 'paul'], (name) => name.length);
-
그러나 테스팅을 위해 할당을 사용하는 방법에는 두 가지 문제가 있음
-
불필요한 변수를 만들어야 함 그래서 일반적인 해결책은 변수 도입 대신 헬퍼 함수를 정의하는 것
function assertType<T>(x: T) {} assertType<number[]>(map(['john', 'paul'], (name) => name.length));
-
두 타입이 동일한지 체크하는 것이 아니라 할당 가능성을 체크
-
객체의 타입을 체크하는 경우
const beatles = ['john', 'paul', 'george', 'ringo']; // 반환된 배열은 {name: string}[] 에 할당 가능하지만, inYellowSubmarine 속성에 대한 부분이 체크되지 않음 assertType<{ name: string }[]>( map(beatles, (name) => ({ name, inYellowSubmarine: name === 'ringo', })) ); // 정상
-
TS의 함수는 매개변수가 더 적은 함수 타입에 할당 가능하다는 문제
const double = (x: number) => 2 * x; assertType<(a: number, b: number) => number>(double); // 정상?!
-
Parameters
와ReturnType
제네릭 타입을 이용해, 함수의 매개변수 타입과 반환 타입만 분리하여 테스트할 수 있음
const double = (x: number) => 2 * x; let p: Parameters<typeof double> = null; assertType<[number, number]>(p); // 🚨 '[number]' 형식의 인수는 '[number, number]' 형식의 매개변수에 할당될 수 없습니다 let r: ReturnType<typeof double> = null; assertType<number>(r); // 정상
map
의 콜백 함수에서 사용하게 되는this
값에 대한 타입 선언 테스트
declare function map<U, V>( array: U[], fn: (this: U[], u: U, i: number, array: U[]) => V ): V[];
-
-
-
타입 시스템 내에서 암시적
any
타입을 발견하기 위해DefinitelyTyped
의 타입 선언을 위한 도구tslint
사용함// tslint는 할당 가능성을 체크하는 대신 각 심벌의 타입을 추출하여 글자 자체가 같은지 비교한다 const beatles = ['john', 'paul', 'george', 'ringo']; map( beatles, function ( name, // $ExpectType string i, // $ExpectType number array // $ExpectType string[] ) { this; // $ExpectType string[] return name.length; // $ExpectType number[] } );
아이템 53: TS 기능보다 ECMAScript 기능 사용하기
-
JS에 새로 추가된 기능은 TS의 초기 기능과 호환성 문제를 발생
- JS의 신규 기능을 그대로 채택하고 TS 초기 버전과 호환성을 포기. 그러나 이미 사용되고 있던 몇 가지 기능(호환성 문제로 지양하는 방식) 있음
-
열거형(enum)
-
몇몇 값의 모음을 나타내는 방식
-
문제점
-
숫자 열거형에 0, 1, 2 외의 다른 숫자가 할당되면 매우 위험
-
상수 열거형(
const enum
)은 런타임에 완전히 제거되어, 문자열 열거형에서 문제를 일으킴 -
preserveConstEnums
플래그를 설정한 상수 열거형은 런타임 코드에 정보를 유지함 -
문자열 열거형은 구조적 타이핑이 아닌 명목적 타이핑을 사용함
-
문자열 열거형의 명목적 타이핑은 JS와 동작이 다르다는 문제가 있음
enum Flavor { VANILLA = 'vanilla', CHOCOLATE = 'chocolate', STRAWBERRY = 'strawberry', } let flavor = Flavor.CHOCOLATE; // 타입이 Flavor flavor = 'strawberry'; // 🚨 'strawberry' 형식은 'Flavor' 형식에 할당할 수 없습니다 // 열거형 대신 리터럴 타입의 유니온 사용을 권장 type Flavor = 'vanilla' | 'chocolate' | 'strawberry';
-
-
-
매개변수 속성
-
생성자의 매개변수를 사용하여 클래스 초기화 시 TS는 간결한 문법을 제공
class Person { constructor(public name: string) {} }
- 문제점
- 실제로는 코드가 늘어남
- 매개변수 속성은 런타임에는 실제로 사용되지만, TS에서는 사용되지 않는 것처럼 보임
- 매개변수 속성과 일반 속성을 섞어서 사용하면 클래스의 설계가 혼란스러워 짐
- 문제점
-
-
네임스페이스와 트리플 슬래시 임포트
// ES2015 스타일의 모듈(import와 export) 사용을 권장 namespace foo { function bar() {} } /// <reference path="other.ts" /> foo.bar();
-
데코레이터
- 클래스, 메서드, 속성에
annotation
을 붙이거나 기능을 추가하는 것 - 문제점
- 표준화가 완료되지 않았기 때문에 비표준으로 바뀌거나 호환성이 깨질 가능성이 있음
- 클래스, 메서드, 속성에
아이템 54: 객체를 순회하는 노하우
-
편집기에서 오류가 발생하는 경우
const obj = { one: 'uno', two: 'dos', three: 'tres', }; for (const k in obj) { const v = obj[k]; // 🚨 obj에 인덱스 시그니처가 없기 때문에 엘리먼트는 암시적으로 'any' 타입 } // k가 string 으로 인식되기 때문 // k의 타입을 더욱 구체적으로 명시해서 해결가능 let k: keyof typeof obj; for (k in obj) { const v = obj[k]; // 정상 }
-
k
가string
으로 추론된 이유// a, b, c 외에 다른 속성이 존재하는 객체도 foo 함수의 매개변수 abc에 할당 가능하기 때문 interface ABC { a: string; b: string; c: number; } function foo(abc: ABC) { for (const k in abc) { const v = abc[k]; // 🚨 } }
-
-
keyof
사용시 문제점v
도string | number
로 한정되어 범위가 너무 좁아짐
-
단지 객체의 키와 값을 순회하고 싶다면
Object.entries
를 사용function foo(abc: ABC) { for (const [k, v] of Object.entries(abc)) { k; // string 타입 v; // any 타입 } }
아이템 55: DOM 계층 구조 이해하기
-
DOM 엘리먼트를 사용할 때 TS 에러
-
EventTarget
: DOM 타입 중 가장 추상화된 타입으로, 이벤트리스너의 추가/제거, 이벤트 보내기만 가능// 'EventTarget' 형식에 'classList' 속성이 없음 // Event의 currentTarget 속성의 타입은 EventTarget | null function handleDrag(eDown: Event) { const targetEl = eDown.currentTarget; targetEl.classList.add('dragging'); }
-
Node
:Element
가 아닌Node
, 텍스트 조각과 주석 -
Element
와HTMLElement
: HTML이 아닌 엘리먼트,SVGSvgElement
-
HTMLxxxElement
-
HTMLxxxElement
형태의 특정 엘리먼트들은 자신만의 고유한 속성을 가지고 있음 ex)HTMLImageElement(src)
,HTMLInputElement(value)
-
항상 정확한 타입을 얻을 수 있는 것은 아님
// 정확한 타입 document.createElement('button'); // HTMLButtonElement // 정확한 타입이 아닌 경우 document.getElementById('my-div'); // HTMLElement
-
타입 단언문 사용
document.getElementById('my-div') as HTMLDivElement;
-
-
strictNullChecks
설정 시, 엘리먼트가null
인 경우를 체크함 -
Event
는 가장 추상화된 이벤트로, 별도의 계층구조를 가짐- ex)
UIEvent, MouseEvent, TouchEvent, WheelEvent, KeyboardEvent
- 더 많은 문맥 정보를 제공하여 DOM에 대한 타입 추론을 가능하게 해야 함
- ex)
-
아이템 56: 정보를 감추는 목적으로 private 사용 X
-
public, protected, private
같은 접근 제어자- TS 키워드기 때문에 컴파일 후에 제거 됨
-
심지어 단언문을 사용하면 TS 상태에서도 private 속성에 접근 가능
// 정보를 감추기 위해 `private` 을 사용하면 안 됨 class Diary { private secret = 'test'; } const diary = new Diary(); (diary as any).secret; // 정상
-
정보를 감추기 위해 클로저 사용
// PasswordChecker 의 생성자 외부에서 passwordHash 변수에 접근할 수 없기 때문에 정보가 숨겨 짐 // 이때 passwordHash에 접근하는 메서드 역시 생성자 내부에 정의되어야 함 // 메서드 정의가 생성자 내부에 있으면, 인스턴스 메서드로 생성된다는 점을 기억(메모리 낭비) declare function hash(text: string): number; class PasswordChecker { checkPassword: (password: string) => boolean; constructor(passwordHash: number) { this.checkPassword = (password: string) => { return hash(password) === passwordHash; }; } } const checker = new PasswordChecker(hash('s3cret')); checker.checkPassword('s3cret'); // true
-
비공개 필드 사용
- 접두사
#
- 타입 체크와 런타임 모두에서 비공개
- 클래스 외부에서는 접근할 수 없지만, 클래스 메서드나 동일 클래스의 개별 인스턴스끼리는 접근이 가능
- 접두사
아이템 57: 소스맵을 사용하여 TS 디버깅하기
-
디버거는 런타임에 동작하며, 현재 동작하는 코드가 어떤 과정을 거쳤는지 모름
-
디버깅 문제를 해결하기 위해 브라우저는 소스맵(source map) 기능을 제공
- 변환된 코드의 위치와 심벌들을 원본 코드의 원래 위치와 심벌들로 매핑함
-
TS의 소스맵 활성화
// tsconfig.json // 각 .ts 파일에 대해서 .js와 .js.map 두 개의 파일을 생성 { "compilerOptions": { "sourceMap": true } }
-
소스맵에 대해 알아야 할 사항들
- TS와 함께 번들러나 압축기를 사용하고 있다면, 번들러나 압축기가 각자의 소스맵을 생성. 이상적인 디버깅을 위해서는 생성된 JS가 아닌 원본 TS 소스로 매핑되도록 해야 함
- 상용 환경에 소스맵이 유출되고 있는지 확인해야 함. 소스맵에 원본 코드의 인라인 복사본이 포함되어 있다면 공개해서는 안될 내용이 들어있을 수 있음
아이템 58: 모던 JS로 작성하기
-
TS의 컴파일러를 JS의 ‘트랜스파일러’로 사용
- TS는 JS의 상위집합이므로 TS를 JS로 컴파일할 수 있음
-
ECMAScript 모듈 사용
- ES2015에 등장한
import
와export
를 사용하는 모듈이 표준이 되었음
- ES2015에 등장한
-
프로토타입 대신 클래스 사용
class Person { first: string; last: string; constructor(first: string, last: string) { this.first = first; this.last = last; } getName() { return this.first + ' ' + this.last; } } const marie = new Person('Marie', 'Curie'); const personName = marie.getName();
-
var
대신let / const
사용- 스코프 문제 피하기
- 함수 선언문 대신 함수 표현식을 사용하여 호이스팅 문제 피하기
-
for(;;)
대신for-of
또는 배열 메서드 사용for-of
루프는 코드가 짧고 인덱스 변수를 사용하지 않아 실수를 줄일 수 있음- 인덱스 변수가 필요한 경우엔
forEach
메서드 사용 권장
-
함수 표현식보다 화살표 함수 사용
- 상위 스코프의
this
를 유지할 수 있음 - 코드를 더 직관적이고 간결하게 작성할 수 있음
- 상위 스코프의
-
단축 객체 표현과 구조 분해 할당 사용
-
변수와 객체 속성의 이름이 같은 경우
const x = 1, y = 2, z = 3; const pt = { x, y, z };
-
객체 속성 중 함수를 축약해서 표현하는 방법
const obj = { onClickLong: function (e) { // ... }, onClickCompact(e) { // ... }, };
-
객체 구조 분해
const { props: { a, b }, } = obj;
-
-
함수 매개변수 기본값 사용
- 기본값을 기반으로 타입 추론이 가능하기 때문에, TS로 마이그레이션 시 매개변수에 타입 구문을 쓰지 않아도 됨
-
저수준 프로미스나 콜백 대신
async / await
사용- 버그나 실수를 방지할 수 있고, 비동기 코드에 타입 정보가 전달되어 타입 추론을 가능하게 함
-
연관 배열에 객체 대신
Map
과Set
사용-
인덱스 시그니처 사용 시 :
constructor
등의 특정 문자열이 주어지는 경우 예약어로 인식하는 문제 -
Map
사용function countWordsMap(text: string) { const counts = new Map<string, number>(); for (const word of text.split(/[\s,.]+/)) { counts.set(word, 1 + (counts.get(word) || 0)); } return counts; }
-
-
TS에
use strict
넣지 않기- 타입스크립트는 기본적으로 'use strict'를 사용
alwaysStrict
또는strict
컴파일러 옵션 설정 권장
-
TC39나 타입스크립트 릴리즈 노트를 통해 최신 기능 확인 가능
아이템 59: TS도입 전, @ts-check와 JSDoc 시도
-
@ts-check
지시자를 사용하여 타입 체커가 파일을 분석하고, 발견된 오류를 보고하도록 지시할 수 있음- 매우 느슨한 수준으로 타입 체크를 수행
- 타입 불일치나 함수의 매개변수 개수 불일치 등
-
선언되지 않은 전역 변수
-
숨어 있는 변수라면 변수를 제대로 인식할 수 있게 별도로 타입 선언 파일을 만들기
// @ts-check console.log(user.firstName); // types.d.ts interface UserData { firstName: string; lastName: string; } declare let user: UserData; // 선언 파일을 찾지 못하는 경우 ‘트리플 슬래시’ 참조를 사용하여 명시적으로 import // @ts-check // <reference path="./types.d.ts" /> console.log(user.firstName); // 정상
-
-
알 수 없는 라이브러리
- 서드파티 라이브러리의 타입 정보
@types/xxx
설치하기
-
DOM 문제
// @ts-check const ageEl = /** @type {HTMLInputElement} */ document.getElementById('age'); ageEl.value = '12'; // 정상
-
부정확한
JSDoc
-
타입스크립트 언어 서비스는 타입을 추론해서 JSDoc을 자동으로 생성
// @ts-check /** * @param {number} val */ function double(val) { return 2 * val; }
-
아이템 60: allowJS로 TS와 JS 같이 사용하기
-
allowJS
옵션- 타입 체크와 관련이 없지만, 기존 빌드 과정에 TS 컴파일러를 추가하기 위함
- 모듈 단위로 TS로 전환하는 과정에서 테스트를 수행하기 위함
-
프레임워크 없이 빌드 체인 직접 구성하기
outDir
옵션 사용하기
아이템 61: 의존성 관계에 따라 모듈 단위로 전환하기
-
의존성 관련 오류 없이 작업하려면, 다른 모듈에 의존하지 않는 최하단 모듈부터 작업을 시작해서 의존성의 최상단에 있는 모듈을 마지막으로 완성해야 함
- 서드파티 라이브러리 타입 정보를 가장 먼저 해결 (
@types/
) - 외부 API의 타입 정보 추가
- 서드파티 라이브러리 타입 정보를 가장 먼저 해결 (
-
리팩터링은 TS 전환 작업이 완료된 후에 해야 함
-
선언되지 않은 클래스 멤버
-
‘누락된 모든 멤버 추가’ 빠른 수정
class Greeting { greeting: string; name: any; // 직접 수정 필요 constructor(name) { this.greeting = 'Hello'; this.name = name; } greet() { return this.greeting + ' ' + this.name; } }
-
-
타입이 바뀌는 값
// 한번에 객체 생성 또는 타입 단언문 사용 const state = {}; state.name = 'New York'; // 🚨 '{}' 유형에 'name' 속성이 없습니다 state.capital = 'Albany'; // 🚨 '{}' 유형에 'capital' 속성이 없습니다
-
JS에서
JSDoc
과@ts-check
를 사용해 타입 정보를 추가한 상태라면, 타입스크립트로 전환하는 순간 타입 정보가 ‘무효화’된다는 점에 주의 -
마지막으로 테스트 코드를 TS로 전환
아이템 62: 마이그레이션을 위해 noImplicitAny 설정
noImplicitAny
설정을 통해 타입 선언과 관련된 실제 오류를 드러낼 수 있음- 최종적으로 가장 강력한 설정은
strict: true