✏️ 아이템 19: 추론 가능한 타입을 사용해 장황한 코드 방지하기
- 코드의 모든 변수에 타입을 선언하는 것은 비 생산적
-
객체는 비구조화 할당문 사용 지향
-
모든 지역 변수의 타입이 추론되도록 해야 함
function logProduct(product: Product) { const { id, name, price } = product; console.log(id, name, price); }
-
-
타입 구문을 생략하는 경우
- 함수 내에서 생성된 지역 변수
- 함수 파라미터에 기본 값이 있는 경우
-
타입을 명시하면 좋은 경우
- 객체 리터럴을 정의할 때, 잉여 속성 체크가 동작 함
-
함수의 반환 타입
- 함수의 입출력 타입에 대해 더욱 명확하게 알 수 있음
-
명명된 타입을 사용할 수 있음
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[][]; bbox?: BoundingBox; } // 속성 체크는 polygon.bbox의 타입을 정제했지만 box는 그렇지 않음 function isPointInPolygon(polygon: Polygon, pt: Coordinate) { polygon.bbox; // 타입이 BoundingBox | undefined const box = polygon.bbox; box; // 타입이 BoundingBox | undefined if (polygon.bbox) { polygon.bbox; // 타입이 BoundingBox box; // 타입이 BoundingBox | undefined } } // 객체 비 구조화 할당 이용 function isPointInPolygon(polygon: Polygon, pt: Coordinate) { const { bbox } = polygon; if (bbox) { const { x, y } = bbox; // ... } } // 객체 비구조화 이용 시 주의사항 // - 전체 bbox 속성이 아니라 x와 y가 선택적 속성일 경우 속성 체크가 더 필요 함 // - bbox에는 선택적 속성이 적합했지만 holes에는 그렇지 않음 // - 런타임에도 혼동을 야기할 가능성 // - 속성보다 지역 변수 사용
✏️ 아이템 25: 비동기 코드에는 콜백 대신 async 함수 사용하기
-
과거 JS의 비동기 콜백 지옥 발생
- ES2015는 Promise 개념을 도입
- ES2017에서는
async/await
도입\ - TS 런타임에 관계없이
async/await
사용 가능 - TS의 프로미스 반환 타입은
Promise<Response>
-
일반적으로 프로미스보다는
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)이 계속 전달 됨
-
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 라이브러리의 동작을 정확히 모델링하기 위해서 개발되었으므로, 라이브러리 사용 시 타입 정보가 잘 유지되는 점을 활용