아이템 19 - 추론 가능한 타입을 사용해 장황한 코드 방지하기
타입 추론이란?
타입스크립트에서 타입 구문을 명시하지 않는다면, 일반적으로 변수의 타입 혹은 함수가 반환하는 타입은 처음 등장할 때 결정되는데, 이것을 타입 추론이라고 합니다.
-
변수의 타입을 명시하지 않고 값을 할당할 경우
let x = 12; // number 타입으로 추론 const person = { name: 'Sojourner Truth', born: { where: 'Swartekill, NY', when: 'c.1797', } }; // { name: string; born: { where: string; when: string; }; } 타입으로 추론 -
함수의 반환 타입을 명시하지 않았을 경우
function square(nums: number[]) { return nums.map(x => x * x); } const squares = square([1, 2, 3, 4]); // number[] 타입으로 추론
이처럼 타입 추론은 타입 구문을 생략하여 장황한 코드를 작성하지 않게 함으로써 코드를 읽는 사람이 구현 로직에 집중할 수 있게 해줍니다.
그러면 언제 타입 추론을 활용하는 것이 좋고, 언제 타입을 명시하는 것이 좋은지 알아봅시다.
타입 추론을 활용하는 경우
-
원시값을 할당
let str = 'string'; const bool = true; const num = 12; -
함수 내에서 생성된 지역 변수
interface Product { id: string; name: string; price: number; } function logProduct(product: Product) { const { id, name, price } = product; // 비구조화 할당문을 사용 /* ... */ } -
함수에서 기본값이 있는 매개변수
function parseNumber(str: string, base = 10) { /* ... */ } -
타입 정보가 있는 라이브러리에서 콜백 함수의 매개변수
namespace express { export interface Request {} export interface Response { send(text: string): void; } } interface App { get(path: string, cb: (request: express.Request, response: express.Response) => void): void; } const app: App = null!; // Don't do this: app.get('/health', (request: express.Request, response: express.Response) => { response.send('OK'); }); // Do this: app.get('/health', (request, response) => { response.send('OK'); });
타입을 명시하는 경우
-
객체 리터럴 정의
객체 리터럴을 정의할 때, 타입을 명시하면 타입스크립트는 잉여 속성 체크를 하기 때문에 객체가 사용되는 곳에서 오류가 발생하는 것이 아니라 객체를 선언한 곳에 오류가 발생합니다.
interface Product { id: string; name: string; price: number; } // 타입을 명시하지 않은 경우 const furby = { id: 630509430963, name: 'Furby', price: 35, }; logProduct(furby); // ~~~~~ Argument .. is not assignable to parameter of type 'Product' // Types of property 'id' are incompatible // Type 'number' is not assignable to type 'string' // 타입을 명시한 경우 const typedFurby: Product = { id: 630509430963, // ~~ Type 'number' is not assignable to type 'string' name: 'Furby', price: 35, }; logProduct(typedFurby); -
함수의 반환 타입
함수의 반환 타입을 명시하면 함수에 대해 더욱 명확하게 알 수 있고, 명명된 타입을 사용할 수 있기 때문에 타입을 명시하는 것이 좋습니다.
interface Vector2D { x: number; y: number; } function add(a: Vector2D, b: Vector2D): Vector2D { return { x: a.x + b.x, y: a.y + b.y }; }
아이템 20- 다른 타입에는 다른 변수 사용하기
변수에 기본형 타입을 할당하는 방법
타입스크립트에서는 변수에 값을 재할당할 때, 변수를 초기화할 때 지정한 타입과 다른 타입의 값을 할당하면 오류가 발생합니다.
function fetchProduct(id: string) { /* ... */ }
function fetchProductBySerialNumber(id: number) { /* ... */ }
let id = "12-34-56";
fetchProduct(id);
id = 123456; // '123456' is not assignable to type 'string'.
fetchProductBySerialNumber(id);
// ~~ Argument of type 'string' is not assignable to
// parameter of type 'number'
위의 오류를 해결하기 위해 id 의 타입을 string | number 와 같은 유니온 타입으로 변경할 수 있지만, 이러한 타입은 간단한 string 이나 number 타입에 비해 다루기가 더 어렵습니다.
이럴 때는 별도의 변수를 도입하는 것이 더 바람직합니다.
function fetchProduct(id: string) { /* ... */ }
function fetchProductBySerialNumber(id: number) { /* ... */ }
const id = "12-34-56";
fetchProduct(id);
const serial = 123456; // OK
fetchProductBySerialNumber(serial); // OK이렇게 별도의 변수를 도입하면 다음과 같은 장점을 가질 수 있습니다.
- 서로 관려이 없는 두 개의 값을 분리합니다. (
id와serial) - 변수명을 더 구체적으로 지을 수 있습니다.
- 타입 추론을 향상시키며, 타입 구문이 불필요해집니다.
- 타입이 좀 더 간결해집니다. (
string | number대신string과number사용 ) let대신const로 변수를 선언하게 되어 코드가 간결해지고, 타입 체커가 타입을 추론하기에도 좋습니다.
따라서 타입이 다른 값을 다룰 때는 변수를 재사용하지 않고 별도의 변수명을 사용하는 것이 좋습니다.
아이템 21 - 타입 넓히기
변수가 초기화 될 때, 타입이 추론되는 과정
타입스크립트에서는 타입을 추론할 때, 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추합니다. 이러한 과정을 **넓히기(widening)**라고 부릅니다.
넓히기 과정에서는 일반적으로 변수가 선언된 후로는 타입이 바뀌지 않는 것을 가정하고 명확성과 유연성 사이의 균형을 유지해서 타입을 추론하려고 합니다.
기본형 값의 넓히기 과정 예시
let x = 'x';변수 x 에는 타입을 명시하지 않았으므로 타입스크립트는 타입 추론을 하게 되는데, 할당 가능한 값들의 집합에는 다음과 같은 후보군이 존재합니다.
anystring'x'
일반적으로 타입을 추론할 때, 변수가 선언된 후로는 타입이 바뀌지 않는 것을 가정하기 때문에 위 후보군 중에서 any 타입은 다른 타입도 할당할 수 있어 후보군에서 제외됩니다. 또한 변수 x 는 let 으로 선언되었기 때문에, 'x'라는 string literal이 아닌 다른 string 으로 재할당될 수 있을 것이라 생각하여 타입스크립트는 최종적으로 변수 x의 타입을 string 으로 추론하게 됩니다.
const x = 'x';위의 예시와 달리 변수 x 는 const 로 선언되었기 때문에, 다른 string 으로 재할당이 불가능하여 타입스크립트는 최종적으로 변수 x 의 타입을 string literal인 'x' 로 추론하게 됩니다.
객체의 넓히기 과정 예시
const v = { x: 1 };변수 v 에 할당 가능한 값들의 집합에는 다음과 같은 후보군이 존재합니다.
{ readonly x: 1 }{ x: number }{ [key: string]: number }- …
객체의 경우 넓히기 알고리즘은 각 요소를 let 으로 할당된 것처럼 다룹니다. 따라서 변수 v 의 타입은 { x: number } 가 됩니다.
타입 추론 강도 제어하기
지금까지 타입스크립트에게 지정된 값만을 가지고 타입을 추론하게 했는데, 다음과 같은 방법으로 타입 추론의 강도를 제어할 수 있습니다.
-
명시적 타입 구문 제공
const v: {x: 1|3|5} = { x: 1, }; // Type is { x: 1 | 3 | 5; } -
추가적인 문맥을 제공 (아이템 26에서 다룸)
-
const단언문 사용// 값 뒤에 as const를 작성하면, 타입스크립트는 최대한 좁은 타입으로 추론합니다. const v1 = { x: 1, y: 2, }; // Type is { x: number; y: number; } const v2 = { x: 1 as const, y: 2, }; // Type is { x: 1; y: number; } const v3 = { x: 1, y: 2, } as const; // Type is { readonly x: 1; readonly y: 2; }
아이템 22 - 타입 좁히기
여러가지 타입이 가능할 때, 문맥에 따라 특정 타입으로 추론하게 하는 방법
타입 좁히기는 타입스크립트가 넓은 타입으로부터 좁은 타입으로 추론하는 과정을 말하는데, 다음과 같은 방법으로 타입을 좁힐 수 있습니다.
-
조건문
const el = document.getElementById('foo'); // Type is HTMLElement | null if (el) { el // Type is HTMLElement } else { el // Type is null } -
예외를 던지거나 함수를 반환(early return)
// 예외를 던지는 방법 const el = document.getElementById('foo'); // Type is HTMLElement | null if (!el) throw new Error('Unable to find #foo'); el; // Now type is HTMLElement -
instanceoffunction contains(text: string, search: string|RegExp) { if (search instanceof RegExp) { search // Type is RegExp return !!search.exec(text); } search // Type is string return text.includes(search); } -
속성 체크
interface A { a: number } interface B { b: number } function pickAB(ab: A | B) { if ('a' in ab) { ab // Type is A } else { ab // Type is B } ab // Type is A | B } -
Array.isArrayfunction contains(text: string, terms: string|string[]) { const termList = Array.isArray(terms) ? terms : [terms]; termList // Type is string[] // ... } -
태그된 유니온(tagged union) 또는 구별된 유니온(discriminated union)
각 객체의 동일한 프로퍼티에 객체마다 다른 값을 지정하여 서로 다른 객체를 구분하는 방법
interface UploadEvent { type: 'upload'; filename: string; contents: string } interface DownloadEvent { type: 'download'; filename: string; } type AppEvent = UploadEvent | DownloadEvent; function handleEvent(e: AppEvent) { switch (e.type) { case 'download': e // Type is DownloadEvent break; case 'upload': e; // Type is UploadEvent break; } } -
타입 식별을 돕기 위한 커스텀 함수
function isInputElement(el: HTMLElement): el is HTMLInputElement { return 'value' in el; } function getElementContent(el: HTMLElement) { if (isInputElement(el)) { el; // Type is HTMLInputElement return el.value; } el; // Type is HTMLElement return el.textContent; }함수의 반환 타입에
el is HTMLInputElement라고 지정하면, 함수의 반환되는 값이true인 경우 타입 체커에게 매개변수의 타입을 좁힐 수 있다고 알려줄 수 있습니다. 이러한 기법을 사용자 정의 타입 가드라고 합니다.
아이템 23 - 한꺼번에 객체 생성하기
변수에 객체를 할당하는 방법
자바스크립트에서는 객체를 생성할 때 다음과 같이 생성할 수 있습니다.
const pt = {};
pt.x = 3;
pt.y = 4;하지만 위의 예제를 타입스크립트에서 사용하면 다음과 같은 오류가 발생합니다.
const pt = {};
pt.x = 3;
// ~ Property 'x' does not exist on type '{}'
pt.y = 4;
// ~ Property 'y' does not exist on type '{}'따라서 타입스크립트에서는 객체를 생성할 때, 속성을 하나씩 추가하기보다는 여러 속성을 포함해서 한꺼번에 생성하는 것이 좋습니다.
타입스트립트에서 객체 생성 방법
-
타입 선언 활용 (
type키워드 or 인터페이스)interface Point { x: number; y: number; } const pt: Point = { x: 3, y: 4, }; // OK -
타입 추론 활용
const pt = { x: 3, y: 4, }; -
타입 단언문 활용
interface Point { x: number; y: number; } const pt = {} as Point; pt.x = 3; pt.y = 4; // OK
타입스크립트에서 여러 객체를 합치는 방법
작은 객체들을 조합해서 큰 객체를 만들어야 하는 경우 **객체 전개 연산자(...)**을 사용하면 됩니다.
interface Point { x: number; y: number; }
const pt = { x: 3, y: 4 };
const id = { name: 'Pythagoras' };
const namedPoint = { ...pt, ...id };
namedPoint.name; // OK, type is string
// 객체 전개 연산자를 사용하면 필드 단위로 객체를 생성할 수도 있습니다.
const pt0 = {};
const pt1 = { ...pt0, x: 3 };
const pt: Point = { ...pt1, y: 4 }; // OK객체에 조건부 속성을 추가하려면, 속성을 추가하지 않는 null 또는 {}으로 객체 전개를 사용하면 됩니다.
// 조건부 속성 추가하는 방법
declare let hasMiddle: boolean;
const firstLast = { first: 'Harry', last: 'Truman' };
const president = { ...firstLast, ...(hasMiddle ? { middle: 'S' } : {}) };// 한꺼번에 여러 속성을 추가하는 경우
declare let hasDates: boolean;
const nameTitle = { name: 'Khufu', title: 'Pharaoh' };
const pharaoh = {
...nameTitle,
...(hasDates ? { start: -2589, end: -2566 } : {})
};
// pharaoh의 타입은 { start: number; end: number; name: string; title: string; } | { name: string; title: string; } 가 된다.
// 만약 { start?: number; end?: number; name: string; title: string; } 타입으로 만들고 싶다면 헬퍼 함수를 사용하면 된다.
function addOptional<T extends object, U extends object>(
a: T, b: U | null
): T & Partial<U> {
return { ...a, ...b };
}
const optionalPharaoh = addOptional(
namedTitle,
hasDates ? { start: -2589, end: -2566 } : null
);
optionalPharaoh.start // OK, type is number | undefined아이템 24 - 일관성 있는 별칭 사용하기
interface Coordinate {
x: number;
y: number;
}
interface BoundingBox {
x: [number, number];
y: [number, number];
}
interface Polygon {
exterior: Coordinate[];
holes: Coordinate[][];
bbox?: BoundingBox;
}
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
if (polygon.bbox) {
if (pt.x < polygon.bbox.x[0] || pt.x > polygon.bbox.x[1] ||
pt.y < polygon.bbox.y[1] || pt.y > polygon.bbox.y[1]) {
return false;
}
}
// ... more complex check
}위 예제에서 isPointInPolygon 함수 내부에서 polygon.bbox 라는 것이 중복되어 사용되고 있어 중복을 제거하는 리팩토링을 해보겠습니다.
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
const { bbox } = polygon; // 비구조화 할당을 이용
if (bbox) {
const {x, y} = bbox;
if (pt.x < x[0] || pt.x > x[1] ||
pt.y < x[0] || pt.y > y[1]) {
return false;
}
}
// ...
}위와 같이 비구조화 할당을 이용하면 보다 간결한 문법으로 일관된 이름을 사용할 수 있습니다.
아이템 26 - 타입 추론에 문맥이 어떻게 사용되는지 이해하기
타입스크립트는 타입을 추론할 때 단순히 값만 고려하지는 않습니다. 값이 존재하는 곳의 문맥까지도 살피는데, 이렇게 문맥을 고려해 타입을 추론하면 가끔 이상한 결과가 나옵니다. 이때 타입 추론에 문맥이 어떻게 사용되는지 이해하고 있다면 제대로 대처할 수 있습니다.
문자열 사용 시 주의점
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) { /* ... */ }
setLanguage('JavaScript'); // 이 문맥에서는 string literal로 타입을 추론하기 때문에 OK
// 오류가 발생하는 경우
let language = 'JavaScript'; // 이 문맥에서는 language를 string 타입으로 추론
setLanguage(language);
// ~~~~~~~~ Argument of type 'string' is not assignable
// to parameter of type 'Language'// 오류 해결책
// 1. let을 그대로 사용하고 타입 명시한다.
let language: Language = 'JavaScript'; // 이 문맥에서는 language를 string literal로 타입을 추론
setLanguage(language); // OK
// 2. const로 변수를 선언한다.
const language = 'JavaScript'; // 이 문맥에서는 language를 string literal로 타입을 추론
setLanguage(language); // OK튜플 사용 시 주의점
function panTo(where: [number, number]) { /* ... */ }
panTo([10, 20]); // 이 문맥에서는 타입을 [number, number]로 추론하기 때문에 OK
// 오류가 발생하는 경우
const loc = [10, 20]; // 이 문맥에서는 loc의 타입을 number[] 타입으로 추론
panTo(loc);
// ~~~ Argument of type 'number[]' is not assignable to
// parameter of type '[number, number]'// 오류 해결책
// 1. 변수에 타입을 명시한다.
const loc: [number, number] = [10, 20];
panTo(loc); // OK
// 2. const 단언문을 사용하고 함수 매개변수의 타입을 readonly로 변경한다.
function panTo(where: readonly [number, number]) { /* ... */ }
const loc = [10, 20] as const; // 이 문맥에서는 loc의 타입을 readonly [number, number] 타입으로 추론
panTo(loc); // OK객체 사용 시 주의점
type Language = 'JavaScript' | 'TypeScript' | 'Python';
interface GovernedLanguage {
language: Language;
organization: string;
}
function complain(language: GovernedLanguage) { /* ... */ }
complain({ language: 'TypeScript', organization: 'Microsoft' }); // 이 문맥에서는 language와 organization을 string literal로 타입을 추론하기 때문에 OK
// 오류가 발생하는 경우
const ts = {
language: 'TypeScript',
organization: 'Microsoft',
};
complain(ts); // 이 문맥에서는 language와 organization을 string 타입으로 추론
// ~~ Argument of type '{ language: string; organization: string; }'
// is not assignable to parameter of type 'GovernedLanguage'
// Types of property 'language' are incompatible
// Type 'string' is not assignable to type 'Language'// 오류 해결책
// 1. 변수에 타입을 명시한다.
const ts: GovernedLanguage = {
language: 'TypeScript',
organization: 'Microsoft',
};
complain(ts); // OK
// 2. const 단언문을 사용한다.
const ts = {
language: 'TypeScript',
organization: 'Microsoft',
} as const;
complain(ts); // OK콜백 사용 시 주의점
function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
fn(Math.random(), Math.random());
}
callWithRandomNumbers((a, b) => { // 이 문맥에서 a와 b를 number 타입으로 추론
a; // Type is number
b; // Type is number
console.log(a + b);
});콜백을 상수로 뽑아내면 문맥이 소실되고 noImplicitAny 오류가 발생하게 됩니다.
function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
fn(Math.random(), Math.random());
}
const fn = (a, b) => {
// ~ Parameter 'a' implicitly has an 'any' type
// ~ Parameter 'b' implicitly has an 'any' type
console.log(a + b);
}
callWithRandomNumbers(fn);// 해결책
// 1. 매개변수에 타입 구문을 추가한다.
const fn = (a: number, b: number) => {
console.log(a + b);
}
callWithRandomNumbers(fn);
// 2. 함수 시그니처를 사용한다.
type CallWithRandomNumbers = (n1: number, n2: number) => void;
const fn: CallWithRandomNumbers = (a, b) => {
console.log(a + b);
}
callWithRandomNumbers(fn);