TypeScript 고급 타입 - 타입 시스템의 진짜 힘
이런 코드를 본 적 있으신가요?
function getProperty(obj: any, key: string): any {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // any 타입 반환
const typo = getProperty(user, "naaaame"); // 오타인데도 오류 없음
any를 사용하면 타입 안전성을 잃게 됩니다. 객체에서 속성을 가져올 때, 그 속성이 존재하는지, 어떤 타입인지 알 수 없죠.
저도 처음 TypeScript를 배울 때, 기본 타입만으로는 복잡한 상황을 표현하기 어렵다는 걸 느꼈습니다. 특히 라이브러리를 만들거나 재사용 가능한 컴포넌트를 작성할 때 any를 남발하게 되더라고요.
하지만 TypeScript의 고급 타입을 알게 된 후, 훨씬 더 안전하고 표현력 있는 코드를 작성할 수 있게 되었습니다.
이 글에서는 TypeScript의 고급 타입 기능을 깊이 있게 다룹니다. 단순히 문법을 나열하는 게 아니라, 왜 이런 기능이 필요한지, 어떻게 실무에서 활용하는지를 중심으로 설명합니다.
먼저, 기초부터 이해하기: 타입 시스템이란?
고급 타입을 다루기 전에, 타입 시스템의 근본 원리를 이해해야 합니다.
타입이란 무엇인가?
타입은 값이 가질 수 있는 형태와 그 값으로 할 수 있는 연산을 정의하는 것입니다.
let num: number = 42;
num.toFixed(2); // ✅ number는 toFixed 메서드를 가지고 있음
num.toUpperCase(); // ❌ number는 toUpperCase 메서드가 없음
타입 시스템은 이런 규칙을 컴파일 시점에 검사하여, 런타임 오류를 미리 방지합니다.
왜 고급 타입이 필요한가?
기본 타입(string, number, boolean 등)만으로는 실제 프로그램의 복잡한 관계를 표현하기 어렵습니다.
예를 들어:
- 함수의 입력 타입에 따라 출력 타입이 달라진다면?
- 객체의 모든 속성을 선택적으로 만들고 싶다면?
- 여러 타입 중 공통 속성만 추출하고 싶다면?
이런 상황에서 고급 타입이 필요합니다.
1. Generics (제네릭) - 타입을 매개변수화하기
왜 Generics가 필요한가?
함수를 작성할 때, 여러 타입에 대해 동일한 로직을 적용하고 싶을 때가 있습니다.
❌ 문제 상황: any 사용
function identity(arg: any): any {
return arg;
}
const result = identity("hello"); // any 타입
// 타입 정보를 잃어버림
any를 사용하면 타입 안전성을 잃습니다.
✅ 해결: Generics 사용
function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>("hello"); // string 타입
const num = identity<number>(42); // number 타입
const auto = identity("hello"); // 타입 추론: string
Generics는 타입을 매개변수처럼 전달할 수 있게 해줍니다.
Generics의 기본 문법
// 함수 제네릭
function func<T>(arg: T): T {
return arg;
}
// 인터페이스 제네릭
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
// 클래스 제네릭
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const stringContainer = new Container<string>("hello");
const numberContainer = new Container<number>(42);
활용: 배열 메서드 구현
JavaScript의 Array.prototype.map을 직접 구현해봅시다.
function map<T, U>(array: T[], callback: (item: T) => U): U[] {
const result: U[] = [];
for (const item of array) {
result.push(callback(item));
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
const doubled = map(numbers, (n) => n * 2); // number[]
const strings = map(numbers, (n) => `${n}`); // string[]
왜 이게 좋을까요?
T는 입력 배열의 타입U는 콜백 함수가 반환하는 타입- 타입 안전성을 유지하면서 여러 타입에 대해 동작
Generic Constraints (제약 조건)
제네릭 타입에 제약을 걸 수 있습니다.
// ❌ 문제: 모든 타입을 받으면 length가 없을 수 있음
function getLength<T>(arg: T): number {
return arg.length; // ❌ 오류: T에 length가 없을 수 있음
}
// ✅ 해결: length 속성이 있는 타입만 받기
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(arg: T): number {
return arg.length; // ✅ T는 반드시 length를 가짐
}
getLength("hello"); // ✅ string은 length를 가짐
getLength([1, 2, 3]); // ✅ array는 length를 가짐
getLength({ length: 10 }); // ✅ 객체에 length가 있음
getLength(42); // ❌ 오류: number는 length가 없음
활용: API 응답 타입
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
name: string;
price: number;
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async function fetchProduct(id: number): Promise<ApiResponse<Product>> {
const response = await fetch(`/api/products/${id}`);
return response.json();
}
// 사용
const userResponse = await fetchUser(1);
console.log(userResponse.data.name); // User 타입으로 추론
const productResponse = await fetchProduct(1);
console.log(productResponse.data.price); // Product 타입으로 추론
왜 이게 유용한가?
하나의 ApiResponse 인터페이스로 다양한 API 응답을 타입 안전하게 처리할 수 있습니다.
2. Keyof Type Operator - 객체 키를 타입으로
keyof 연산자는 객체 타입의 모든 키를 유니온 타입으로 추출합니다.
기본 사용법
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User; // "id" | "name" | "email"
const key1: UserKeys = "id"; // ✅
const key2: UserKeys = "name"; // ✅
const key3: UserKeys = "age"; // ❌ 오류: "age"는 User의 키가 아님
활용: 타입 안전한 getProperty
서두에서 봤던 any를 사용한 코드를 개선해봅시다.
❌ 문제 코드
function getProperty(obj: any, key: string): any {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // any 타입
const typo = getProperty(user, "naaaame"); // 오타인데도 오류 없음
✅ 개선: keyof 사용
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string 타입
const age = getProperty(user, "age"); // number 타입
const typo = getProperty(user, "naaaame"); // ❌ 컴파일 오류!
어떻게 동작하는가?
K extends keyof T:K는T의 키 중 하나여야 함T[K]:T객체의K속성의 타입
Before & After 비교
❌ JavaScript (런타임 오류)
function updateUser(user, key, value) {
user[key] = value;
}
const user = { name: "Alice", age: 30 };
updateUser(user, "name", "Bob"); // ✅
updateUser(user, "age", "30"); // ⚠️ age는 number인데 string을 넣음
updateUser(user, "address", "Seoul"); // ⚠️ address라는 키는 없음
✅ TypeScript (컴파일 시점에 오류 발견)
function updateUser<T, K extends keyof T>(user: T, key: K, value: T[K]): void {
user[key] = value;
}
const user = { name: "Alice", age: 30 };
updateUser(user, "name", "Bob"); // ✅
updateUser(user, "age", 30); // ✅
updateUser(user, "age", "30"); // ❌ 오류: string은 number에 할당 불가
updateUser(user, "address", "Seoul"); // ❌ 오류: "address"는 user의 키가 아님
3. Mapped Types - 타입 변환하기
Mapped Types는 기존 타입을 변환하여 새로운 타입을 만드는 기능입니다.
기본 문법
type Mapped<T> = {
[K in keyof T]: T[K];
};
이 패턴을 이용해 다양한 유틸리티 타입을 만들 수 있습니다.
실전 예제 1: Partial - 모든 속성을 선택적으로
interface User {
id: number;
name: string;
email: string;
}
// 수동으로 작성하면...
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
// Mapped Type으로 자동 생성
type Partial<T> = {
[K in keyof T]?: T[K];
};
type PartialUser2 = Partial<User>;
// { id?: number; name?: string; email?: string; }
왜 유용한가?
업데이트 API에서 일부 필드만 수정할 때 유용합니다.
function updateUser(id: number, updates: Partial<User>): void {
// updates는 User의 일부 속성만 포함 가능
}
updateUser(1, { name: "Bob" }); // ✅
updateUser(1, { email: "bob@example.com" }); // ✅
updateUser(1, { name: "Bob", age: 30 }); // ❌ 오류: age는 User에 없음
실전 예제 2: Readonly - 모든 속성을 읽기 전용으로
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
interface Config {
apiUrl: string;
timeout: number;
}
const config: Readonly<Config> = {
apiUrl: "https://api.example.com",
timeout: 5000
};
config.timeout = 3000; // ❌ 오류: readonly 속성은 수정 불가
실전 예제 3: Pick - 특정 속성만 선택
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 민감한 정보를 제외한 공개 프로필
type PublicProfile = Pick<User, "id" | "name">;
// { id: number; name: string; }
const profile: PublicProfile = {
id: 1,
name: "Alice"
// email과 password는 없음
};
실전 예제 4: Omit - 특정 속성 제외
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 비밀번호를 제외한 안전한 사용자 정보
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; }
function displayUser(user: SafeUser): void {
console.log(user.name);
console.log(user.password); // ❌ 오류: password는 SafeUser에 없음
}
실전 예제 5: Record - 키-값 객체 타입 생성
type Record<K extends string | number | symbol, T> = {
[P in K]: T;
};
// 국가별 환율 정보
type Currency = "USD" | "EUR" | "KRW" | "JPY";
type ExchangeRates = Record<Currency, number>;
const rates: ExchangeRates = {
USD: 1.0,
EUR: 0.85,
KRW: 1200,
JPY: 110
};
// ✅ 모든 통화가 포함됨
// ❌ 하나라도 빠지면 오류
활용:
// API 엔드포인트 맵핑
type Endpoint = "users" | "products" | "orders";
type ApiEndpoints = Record<Endpoint, string>;
const endpoints: ApiEndpoints = {
users: "/api/users",
products: "/api/products",
orders: "/api/orders"
};
function fetchData(endpoint: Endpoint) {
return fetch(endpoints[endpoint]);
}
4. Conditional Types - 조건부 타입
Conditional Types는 조건에 따라 다른 타입을 반환합니다.
기본 문법
T extends U ? X : Y
T가 U에 할당 가능하면 X, 아니면 Y
실전 예제 1: 타입 필터링
// number 타입만 추출
type ExtractNumbers<T> = T extends number ? T : never;
type Test1 = ExtractNumbers<string>; // never
type Test2 = ExtractNumbers<number>; // number
type Test3 = ExtractNumbers<string | number>; // number
실전 예제 2: 함수 반환 타입 추론
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; }
왜 유용한가?
함수의 반환 타입을 자동으로 추론하여, 타입을 중복 정의하지 않아도 됩니다.
❌ 중복 정의
interface User {
id: number;
name: string;
}
function getUser(): User {
return { id: 1, name: "Alice" };
}
// 타입을 두 번 정의함
✅ 자동 추론
function getUser() {
return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>;
// 타입을 한 번만 정의
실전 예제 3: Nullable 타입 제거
type NonNullable<T> = T extends null | undefined ? never : T;
type Nullable = string | number | null | undefined;
type NonNull = NonNullable<Nullable>; // string | number
실전 예제 4: Promise 언래핑
type Awaited<T> = T extends Promise<infer U> ? U : T;
type AsyncString = Promise<string>;
type SyncString = Awaited<AsyncString>; // string
async function fetchUser(): Promise<{ id: number; name: string }> {
// ...
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string; }
5. Template Literal Types - 문자열 타입 조합
Template Literal Types는 문자열 리터럴 타입을 조합하여 새로운 타입을 만듭니다.
기본 사용법
type Greeting = `Hello, ${string}!`;
const g1: Greeting = "Hello, World!"; // ✅
const g2: Greeting = "Hello, Alice!"; // ✅
const g3: Greeting = "Hi, World!"; // ❌ 오류: "Hi"로 시작
실전 예제 1: HTTP 메서드와 경로 조합
type Method = "GET" | "POST" | "PUT" | "DELETE";
type Path = "/users" | "/products" | "/orders";
type Endpoint = `${Method} ${Path}`;
// "GET /users" | "GET /products" | "GET /orders" |
// "POST /users" | "POST /products" | ...
function handleRequest(endpoint: Endpoint) {
console.log(endpoint);
}
handleRequest("GET /users"); // ✅
handleRequest("POST /products"); // ✅
handleRequest("GET /invalid"); // ❌ 오류
실전 예제 2: 이벤트 핸들러 타입
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
interface ButtonProps {
onClick?: () => void;
onFocus?: () => void;
onBlur?: () => void;
}
실전 예제 3: CSS 속성 생성
type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type LonghandProperty = `${CSSProperty}${Capitalize<Direction>}`;
// "marginTop" | "marginRight" | "marginBottom" | "marginLeft" |
// "paddingTop" | "paddingRight" | ...
interface CSSStyles {
marginTop?: string;
marginRight?: string;
paddingTop?: string;
// ...
}
6. Utility Types - 내장 유틸리티 타입
TypeScript는 자주 사용하는 타입 변환을 위한 유틸리티 타입을 내장하고 있습니다.
전체 목록
// 1. Partial<T> - 모든 속성을 선택적으로
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 2. Required<T> - 모든 속성을 필수로
type Required<T> = {
[P in keyof T]-?: T[P];
};
// 3. Readonly<T> - 모든 속성을 읽기 전용으로
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 4. Pick<T, K> - 특정 속성만 선택
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 5. Omit<T, K> - 특정 속성 제외
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// 6. Record<K, T> - 키-값 객체 생성
type Record<K extends string | number | symbol, T> = {
[P in K]: T;
};
// 7. Exclude<T, U> - T에서 U 제외
type Exclude<T, U> = T extends U ? never : T;
// 8. Extract<T, U> - T와 U의 교집합
type Extract<T, U> = T extends U ? T : never;
// 9. NonNullable<T> - null과 undefined 제거
type NonNullable<T> = T extends null | undefined ? never : T;
// 10. ReturnType<T> - 함수 반환 타입 추출
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// 11. Parameters<T> - 함수 매개변수 타입 추출
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
// 12. Awaited<T> - Promise 언래핑
type Awaited<T> = T extends Promise<infer U> ? U : T;
활용 조합
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// 1. 생성 시 사용 (id, createdAt 제외)
type CreateUserInput = Omit<User, "id" | "createdAt">;
// 2. 업데이트 시 사용 (일부 필드만)
type UpdateUserInput = Partial<Omit<User, "id" | "createdAt">>;
// 3. 공개 프로필 (민감 정보 제외)
type PublicProfile = Omit<User, "password" | "email">;
// 4. 읽기 전용 사용자
type ReadonlyUser = Readonly<User>;
7. Type Guards - 타입 좁히기
Type Guards는 런타임에 타입을 확인하여 TypeScript가 타입을 좁힐 수 있게 합니다.
typeof 가드
function processValue(value: string | number) {
if (typeof value === "string") {
// 여기서 value는 string
console.log(value.toUpperCase());
} else {
// 여기서 value는 number
console.log(value.toFixed(2));
}
}
instanceof 가드
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // Dog 타입
} else {
animal.meow(); // Cat 타입
}
}
in 연산자 가드
interface Bird {
fly: () => void;
}
interface Fish {
swim: () => void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly(); // Bird 타입
} else {
animal.swim(); // Fish 타입
}
}
사용자 정의 Type Guard
interface User {
id: number;
name: string;
email: string;
}
interface Admin {
id: number;
name: string;
email: string;
permissions: string[];
}
// Type Guard 함수
function isAdmin(user: User | Admin): user is Admin {
return "permissions" in user;
}
function greet(user: User | Admin) {
if (isAdmin(user)) {
// 여기서 user는 Admin
console.log(`Admin ${user.name} with ${user.permissions.length} permissions`);
} else {
// 여기서 user는 User
console.log(`User ${user.name}`);
}
}
8. 실전 패턴: 타입 안전한 Redux Action
Redux에서 액션을 타입 안전하게 만드는 패턴입니다.
❌ 타입 안전하지 않은 코드
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
function reducer(state: number, action: any) {
switch (action.type) {
case INCREMENT:
return state + action.payload; // payload가 있다는 보장 없음
case DECREMENT:
return state - action.payload;
default:
return state;
}
}
// 오타가 있어도 오류 없음
dispatch({ type: "INCREAMENT", payload: 1 });
✅ 타입 안전한 코드
// 1. 액션 타입 정의
type IncrementAction = {
type: "INCREMENT";
payload: number;
};
type DecrementAction = {
type: "DECREMENT";
payload: number;
};
type ResetAction = {
type: "RESET";
};
// 2. 모든 액션의 유니온
type Action = IncrementAction | DecrementAction | ResetAction;
// 3. 액션 생성자
function increment(amount: number): IncrementAction {
return { type: "INCREMENT", payload: amount };
}
function decrement(amount: number): DecrementAction {
return { type: "DECREMENT", payload: amount };
}
function reset(): ResetAction {
return { type: "RESET" };
}
// 4. 리듀서
function reducer(state: number, action: Action): number {
switch (action.type) {
case "INCREMENT":
return state + action.payload; // ✅ payload 보장
case "DECREMENT":
return state - action.payload; // ✅ payload 보장
case "RESET":
return 0;
default:
const exhaustiveCheck: never = action;
throw new Error(`Unhandled action: ${exhaustiveCheck}`);
}
}
// 사용
dispatch(increment(1)); // ✅
dispatch(decrement(1)); // ✅
dispatch({ type: "RESET" }); // ✅
dispatch({ type: "INCREAMENT" }); // ❌ 오류: 오타
9. 실전 패턴: 타입 안전한 API 클라이언트
목표
const api = createApiClient({
baseUrl: "https://api.example.com"
});
// 타입 안전하게 API 호출
const user = await api.get("/users/:id", { params: { id: 1 } });
// user는 자동으로 올바른 타입
구현
type PathParams<Path extends string> =
Path extends `:${infer Param}/${infer Rest}`
? { [K in Param | keyof PathParams<Rest>]: string | number }
: Path extends `:${infer Param}`
? { [K in Param]: string | number }
: {};
interface ApiEndpoints {
"GET /users/:id": { id: number; name: string; email: string };
"GET /posts/:id": { id: number; title: string; content: string };
"POST /users": { name: string; email: string };
}
type ExtractMethod<T extends string> = T extends `${infer Method} ${string}` ? Method : never;
type ExtractPath<T extends string> = T extends `${string} ${infer Path}` ? Path : never;
function createApiClient() {
return {
async get<E extends Extract<keyof ApiEndpoints, `GET ${string}`>>(
endpoint: ExtractPath<E>,
options: { params: PathParams<ExtractPath<E>> }
): Promise<ApiEndpoints[E]> {
// 실제 API 호출 로직
return {} as ApiEndpoints[E];
}
};
}
// 사용
const api = createApiClient();
const user = await api.get("/users/:id", { params: { id: 1 } });
// user: { id: number; name: string; email: string }
함정과 주의사항
❌ 실수 1: 제네릭 남용
// ❌ 불필요한 제네릭
function add<T extends number>(a: T, b: T): number {
return a + b;
}
// ✅ 단순하게
function add(a: number, b: number): number {
return a + b;
}
언제 제네릭을 사용할까?
- 입력 타입과 출력 타입이 연관될 때
- 여러 타입에 대해 동일한 로직을 재사용할 때
❌ 실수 2: any로 도피
// ❌ any로 문제 회피
function processData(data: any): any {
return data;
}
// ✅ 제네릭 사용
function processData<T>(data: T): T {
return data;
}
❌ 실수 3: 과도하게 복잡한 타입
// ❌ 가독성이 떨어지는 타입
type ComplexType<T> = T extends Array<infer U>
? U extends { [K in keyof U]: infer V }
? V extends string
? Uppercase<V>
: never
: never
: never;
// ✅ 단계별로 나누기
type ExtractArrayItem<T> = T extends Array<infer U> ? U : never;
type ExtractStringValues<T> = T[keyof T] extends string ? T[keyof T] : never;
type UppercaseValues<T> = Uppercase<ExtractStringValues<T>>;
⚠️ 주의: 타입 추론의 한계
TypeScript는 모든 것을 추론할 수 없습니다.
// 추론이 부정확할 수 있음
const config = {
production: { apiUrl: "https://api.example.com" },
development: { apiUrl: "http://localhost:3000" }
};
// ✅ 명시적 타입으로 개선
interface Config {
apiUrl: string;
}
const config: Record<"production" | "development", Config> = {
production: { apiUrl: "https://api.example.com" },
development: { apiUrl: "http://localhost:3000" }
};
활용 팁
1. 타입을 먼저 설계하라
코드를 작성하기 전에 타입부터 설계하면, 더 안전한 코드를 작성할 수 있습니다.
// 1. 먼저 타입 설계
interface User {
id: number;
name: string;
email: string;
}
type CreateUserInput = Omit<User, "id">;
type UpdateUserInput = Partial<CreateUserInput>;
// 2. 함수 구현
function createUser(input: CreateUserInput): User {
// ...
}
function updateUser(id: number, input: UpdateUserInput): User {
// ...
}
2. 유틸리티 타입을 적극 활용하라
TypeScript의 내장 유틸리티 타입을 사용하면 코드 중복을 줄일 수 있습니다.
// ❌ 중복 코드
interface User {
id: number;
name: string;
email: string;
}
interface PartialUser {
id?: number;
name?: string;
email?: string;
}
// ✅ 유틸리티 타입 활용
type PartialUser = Partial<User>;
3. Type Guard를 활용하라
런타임 타입 체크와 TypeScript 타입 좁히기를 결합하면 더 안전합니다.
function isString(value: unknown): value is string {
return typeof value === "string";
}
function processValue(value: unknown) {
if (isString(value)) {
// value는 string으로 좁혀짐
console.log(value.toUpperCase());
}
}
마치며
TypeScript의 고급 타입은 처음에는 복잡해 보이지만, 타입 시스템의 진짜 힘을 발휘하게 해줍니다.
- Generics로 재사용 가능한 컴포넌트 작성
- Mapped Types로 타입 변환 자동화
- Conditional Types로 조건부 로직을 타입에 적용
- Template Literal Types로 문자열 타입 조합
- Type Guards로 런타임 안전성 확보
저도 처음에는 “이렇게까지 해야 하나?”라고 생각했지만, 고급 타입을 사용하면서:
- 리팩토링이 안전해졌고
- 버그가 눈에 띄게 줄었으며
- 코드 자동완성이 훨씬 정확해졌습니다
여러분도 고급 타입에 익숙해지면, TypeScript의 진짜 가치를 느끼실 거예요!
참고 자료
- TypeScript 공식 문서 - Advanced Types
- TypeScript Deep Dive - Advanced Types
- TypeScript Playground - 고급 타입 실습
댓글