이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성.
Effective TypeScript 2판의 Item 16–21을 바탕으로 index signature, 타입 추론 기본, 변수/객체 생성 패턴을 프론트엔드 코드 리뷰 관점에서 정리합니다.
Day 4는 Effective TypeScript 2판의 Item 16–21을 묶어서 읽습니다.
Item 16–21: index signature, inference 기본, 변수/객체 생성 패턴
오늘의 질문은 이겁니다.
타입을 어디에 써야 추론이 더 강해지는가?
TypeScript를 처음 쓰면 모든 변수에 타입을 붙이고 싶어집니다. 하지만 실제 리뷰에서는 반대 상황이 많습니다. 너무 일찍 넓은 타입을 붙이면 TypeScript가 이미 알고 있던 literal 정보, key 이름, undefined 가능성을 잃습니다. 반대로 아무 타입도 안 쓰면 API 경계가 흐려집니다.
Day 4는 “타입을 쓰지 말자”가 아니라 “추론이 잘하는 일과 annotation이 필요한 일을 나누자”는 감각입니다.
추론을 살리는 기본 흐름은 단순합니다.
any는 마지막 수단으로 둔다.이 네 가지가 지켜지면 타입 선언이 줄어도 체크는 더 강해질 수 있습니다.
다음 타입은 편해 보입니다.
type Labels = {
[key: string]: string;
};
const labels: Labels = {
home
Post Q&A
이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성 패턴 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
문제는 key가 너무 넓다는 점입니다.
labels.home; // string
labels.admin; // string으로 보일 수 있음
admin이라는 key가 실제로 없는데도 타입만 보면 string처럼 보입니다. 런타임에서는 undefined일 수 있습니다. 이런 타입은 “아무 문자열 key나 가능하다”는 강한 선언입니다. 단순히 “여러 key가 있는 객체”라는 뜻이 아닙니다.
가능하면 key 집합을 좁히는 편이 낫습니다.
type Page = "home" | "blog" | "about";
type Labels = Record<Page, string>;
const labels: Labels = {
home: "홈",
blog: "블로그",
about: "소개",
};
이제 빠진 key와 잘못된 key를 TypeScript가 잡을 수 있습니다. 리뷰 질문은 이렇게 바뀝니다.
정말 모든 string key를 허용해야 하는가?
가능한 key 목록이 도메인에 이미 존재하지 않는가?
값을 바로 만들면 TypeScript는 꽤 많은 정보를 압니다.
const status = "idle";
// type: "idle"
let nextStatus = "idle";
// type: string
const는 값이 바뀌지 않으므로 literal type으로 좁힐 수 있습니다. let은 나중에 다른 문자열이 들어올 수 있으니 string으로 넓힙니다. 객체도 비슷합니다.
const request = {
method: "GET",
retry: 2,
};
// method는 보통 string으로 넓어질 수 있음
이 객체를 fetch 옵션처럼 더 구체적인 계약에 맞춰야 한다면, 값 생성 지점에서 의도를 드러내야 합니다.
const request = {
method: "GET",
retry: 2,
} as const;
또는 실제 경계 타입에 맞춰 검사하게 할 수 있습니다.
const request = {
method: "GET",
retry: 2,
} satisfies {
method: "GET" | "POST";
retry: number;
};
satisfies는 값의 세부 타입을 최대한 보존하면서도 특정 shape를 만족하는지 검사할 수 있어, 설정 객체나 라우트 테이블 리뷰에 특히 좋습니다.
모든 변수에 타입을 붙이면 안전해 보입니다.
const routes: Record<string, string> = {
home: "/",
blog: "/blog",
};
하지만 이렇게 쓰면 home과 blog라는 구체 key가 사라지고 “문자열 key를 가진 string dictionary”가 됩니다. 다음 코드도 타입상 어색하지 않아 보일 수 있습니다.
routes.admn; // 오타인데 타입이 놓치기 쉬움
반대로 객체 자체의 정보를 살리면 오타를 더 잘 잡습니다.
const routes = {
home: "/",
blog: "/blog",
} as const;
type RouteName = keyof typeof routes;
function navigate(name: RouteName) {
location.href = routes[name];
}
navigate("admn"); // 에러
annotation은 필요합니다. 다만 “정보를 보존한 뒤 경계를 만들 것인지”, “처음부터 넓은 타입으로 덮어버릴 것인지”를 구분해야 합니다.
JavaScript에서는 이런 식의 점진적 조립이 흔합니다.
const article = {};
article.slug = raw.slug;
article.title = raw.title;
article.description = raw.summary ?? "";
TypeScript에서는 시작점이 {}라서 문제가 됩니다. 어떤 필드가 언제 생기는지 추론하기 어렵고, 결국 assertion이나 any로 밀어붙이기 쉽습니다.
더 좋은 기본형은 한 번에 객체를 만드는 것입니다.
const article = {
slug: raw.slug,
title: raw.title,
description: raw.summary ?? "",
};
API mapper라면 반환 타입을 경계에 둡니다.
type ArticleCard = {
slug: string;
title: string;
description: string;
};
function toArticleCard(raw: RawArticle): ArticleCard {
return {
slug: raw.slug,
title: raw.title,
description: raw.summary ?? "",
};
}
이 방식은 객체 생성 위치와 계약 위치가 가까워서 리뷰하기 쉽습니다. 누락된 필드, 남는 필드, nullable 처리도 return 객체에서 한눈에 드러납니다.
문맥이 충분한 곳에서는 타입을 반복하지 않는 편이 낫습니다.
const ids = articles.map((article) => article.id);
articles가 Article[]라면 callback의 article 타입은 문맥으로 추론됩니다. 여기에 굳이 다시 타입을 붙이면 코드가 길어지고, 원본 타입이 바뀔 때 중복 수정 지점이 생깁니다.
const ids = articles.map((article: Article) => article.id);
이런 annotation은 대부분 불필요합니다. 더 나쁜 경우는 틀린 타입을 붙여서 문맥 추론을 방해하는 것입니다.
리뷰에서는 이렇게 묻습니다.
이 annotation은 새로운 계약을 세우는가, 이미 아는 정보를 반복하는가?
새 계약이면 남깁니다. 반복이면 지워도 체크가 유지되는지 확인합니다.
type Messages = Record<string, string>;
외부 CMS처럼 임의 key가 진짜로 올 수 있다면 괜찮습니다. 하지만 라우트, 탭, status, feature flag처럼 가능한 key가 닫혀 있다면 union key나 as const 테이블이 더 안전합니다.
satisfies가 어울리는가const tabs = {
home: { label: "홈" },
code: { label: "코드" },
} satisfies Record<string, { label: string }>;
satisfies는 shape 검사를 하면서 객체 자체의 key 정보를 보존할 수 있습니다. 메뉴, 라우트, 디자인 토큰, feature flag 테이블에 자주 맞습니다.
function map(raw: Raw): ViewModel {
return { ... };
}
중간에 빈 객체를 만들고 assertion으로 채우는 코드보다, 최종 반환 객체를 한 번에 만들면 누락/nullable/default 처리가 보입니다.
배열 메서드, React props callback, 이벤트 핸들러처럼 문맥이 강한 곳에서는 불필요한 annotation이 오히려 노이즈가 됩니다. 타입을 많이 쓰는 것이 아니라, 타입이 필요한 위치에 쓰는 것이 중요합니다.
Day 4의 핵심은 “추론을 믿을 곳과 경계를 세울 곳을 나누기”입니다.
Record<Union, T>, as const, keyof typeof로 표현하는 편이 안전하다.satisfies는 설정 객체처럼 “검사도 하고 세부 정보도 보존”하고 싶은 곳에 잘 맞는다.이 감각이 생기면 TypeScript 코드 리뷰가 “타입이 있네/없네”에서 “이 타입 배치가 체크를 강하게 만들고 있나”로 바뀝니다.
keyof, typeof, satisfiesEffective TypeScript 2판의 Item 11–15를 바탕으로 excess property check, 함수식 타입, type vs interface, readonly, 타입 반복 제거를 프론트엔드 코드 리뷰 관점에서 정리합니다.