Ch 01. any를 피하는 10가지 방법
any는 TypeScript의 탈출구입니다. 타입을 모르겠을 때, 빠르게 컴파일 에러를 없애고 싶을 때 손이 가는 키워드죠. 그런데 any를 하나 쓰면 그 주변 코드 전체의 타입 안전성이 무너집니다. any는 전염됩니다.
이 챕터에서는 실무에서 any가 등장하는 10가지 상황을 짚고, 각 상황에 맞는 구체적인 대안을 제시합니다.
any가 위험한 이유
// 파일: src/any-danger.tsfunction processData(data: any) { // TypeScript가 아무것도 검사하지 않음 return data.user.name.toUpperCase(); // 런타임에 터질 수 있음}processData(null); // 컴파일 통과 → 런타임 에러processData(42); // 컴파일 통과 → 런타임 에러processData({ user: {} }); // 컴파일 통과 → 런타임 에러
컴파일러가 침묵하는 순간, 에러는 사용자에게 도달합니다.
방법 1. unknown으로 교체하기
외부에서 들어오는 데이터의 타입을 모를 때 any 대신 unknown을 씁니다. unknown은 "타입을 모른다"는 의미를 유지하면서 사용 전에 반드시 타입을 확인하도록 강제합니다.
// 파일: src/use-unknown.ts// any 버전 — 위험function parseResponseAny(data: any) { return data.items.length; // 에러 없이 통과}// unknown 버전 — 안전function parseResponseUnknown(data: unknown) { // data를 바로 쓸 수 없음 → 타입 검사 강제 if ( typeof data === "object" && data !== null && "items" in data && Array.isArray((data as { items: unknown }).items) ) { return (data as { items: unknown[] }).items.length; } throw new Error("예상치 못한 응답 형식입니다.");}
방법 2. 제네릭으로 타입을 전달받기
함수가 다양한 타입을 처리해야 할 때 any를 쓰는 경우가 많습니다. 제네릭이 정확한 해법입니다.
// 파일: src/use-generic.ts// any 버전function getFirstAny(arr: any[]): any { return arr[0];}const first = getFirstAny([1, 2, 3]); // first: any// 제네릭 버전function getFirst<T>(arr: T[]): T | undefined { return arr[0];}const num = getFirst([1, 2, 3]); // num: number | undefinedconst str = getFirst(["a", "b", "c"]); // str: string | undefined// 제약이 있는 제네릭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"); // stringconst age = getProperty(user, "age"); // number// getProperty(user, "email"); // 컴파일 에러 — 올바른 경고
방법 3. 타입 가드로 런타임 검증하기
서버에서 받은 데이터를 처리할 때 any로 단언하는 대신 타입 가드를 만듭니다.
// 파일: src/type-guard.tsinterface ApiUser { id: number; name: string; email: string;}function isApiUser(value: unknown): value is ApiUser { return ( typeof value === "object" && value !== null && "id" in value && "name" in value && "email" in value && typeof (value as ApiUser).id === "number" && typeof (value as ApiUser).name === "string" && typeof (value as ApiUser).email === "string" );}async function fetchUser(id: number): Promise<ApiUser> { const response = await fetch(`/api/users/${id}`); const data: unknown = await response.json(); if (!isApiUser(data)) { throw new Error("API 응답이 예상된 형식이 아닙니다."); } return data; // 여기서부터 ApiUser로 안전하게 사용}
방법 4. Record 타입으로 동적 키 처리하기
키를 미리 알 수 없는 객체에 { [key: string]: any } 대신 Record를 씁니다.
// 파일: src/use-record.ts// 나쁜 패턴const configBad: { [key: string]: any } = { timeout: 3000, retries: 3, debug: true,};// 좋은 패턴 — 값 타입을 명시const config: Record<string, string | number | boolean> = { timeout: 3000, retries: 3, debug: true,};// 더 좋은 패턴 — 키와 값 모두 제한type ConfigKey = "timeout" | "retries" | "debug";type ConfigValue = string | number | boolean;const strictConfig: Record<ConfigKey, ConfigValue> = { timeout: 3000, retries: 3, debug: true,};
방법 5. 유니온 타입으로 가능성 열거하기
상태나 종류를 나타내는 필드에 string이나 any 대신 유니온을 씁니다.
// 파일: src/use-union.ts// 나쁜 패턴interface OrderBad { status: any; paymentMethod: string;}// 좋은 패턴type OrderStatus = "pending" | "confirmed" | "shipping" | "delivered" | "cancelled";type PaymentMethod = "card" | "transfer" | "paypal";interface Order { id: string; status: OrderStatus; paymentMethod: PaymentMethod; amount: number;}function updateStatus(order: Order, newStatus: OrderStatus): Order { return { ...order, status: newStatus };}// updateStatus(order, "processing"); // 컴파일 에러 — 잘못된 상태 즉시 감지
방법 6. satisfies 연산자로 타입 유추 유지하기
타입을 검사하면서도 리터럴 타입을 잃지 않으려면 satisfies를 씁니다. TypeScript 4.9에서 도입된 기능입니다.
// 파일: src/use-satisfies.tstype ColorMap = Record<string, string | [number, number, number]>;// as 단언 — 리터럴 정보 손실const paletteAs = { primary: "#3B82F6", secondary: [59, 130, 246],} as ColorMap;// paletteAs.primary는 string | [number, number, number]// satisfies — 타입 검사 + 리터럴 유추 유지const palette = { primary: "#3B82F6", secondary: [59, 130, 246],} satisfies ColorMap;// palette.primary는 string (리터럴 정보 유지)// palette.secondary는 number[] (배열 메서드 사용 가능)palette.secondary.map((v) => v * 2); // 정상 동작
방법 7. 오버로드로 다양한 입력 처리하기
입력 타입에 따라 반환 타입이 달라지는 함수에 any를 쓰는 경우가 있습니다. 함수 오버로드가 해법입니다.
// 파일: src/use-overload.ts// any 버전function formatAny(value: any): any { if (typeof value === "number") return value.toFixed(2); if (typeof value === "Date") return value.toISOString(); return String(value);}// 오버로드 버전function format(value: number): string;function format(value: Date): string;function format(value: string): string;function format(value: number | Date | string): string { if (typeof value === "number") return value.toFixed(2); if (value instanceof Date) return value.toISOString(); return value;}const price = format(1234.5); // stringconst date = format(new Date()); // stringconst text = format("hello"); // string
방법 8. 타입 유틸리티로 기존 타입 변형하기
새 타입을 만들 때 any를 섞는 대신 내장 유틸리티 타입을 활용합니다.
// 파일: src/use-utility.tsinterface User { id: number; name: string; email: string; password: string; createdAt: Date;}// 생성 시 필요한 필드만 (id, createdAt 제외)type CreateUserInput = Omit<User, "id" | "createdAt">;// 수정 시 모든 필드 선택적type UpdateUserInput = Partial<Omit<User, "id" | "createdAt">>;// API 응답 — 비밀번호 제외type PublicUser = Omit<User, "password">;// 검색 조건 — 특정 필드만type UserFilter = Pick<User, "name" | "email">;function createUser(input: CreateUserInput): User { return { ...input, id: Date.now(), createdAt: new Date(), };}
방법 9. 점진적 타이핑 전략 (as unknown as T)
레거시 코드나 외부 라이브러리를 다룰 때 어쩔 수 없이 타입 단언이 필요한 경우가 있습니다. as any 대신 as unknown as T를 씁니다.
// 파일: src/gradual-typing.ts// as any — 최악const userBad = response.data as any;// as unknown as T — 의도를 명확히 표현// "나는 이 데이터가 User라는 것을 알고 있고, 책임진다"const user = response.data as unknown as User;// 더 나은 방법 — 검증 함수와 함께function assertIsUser(value: unknown): asserts value is User { if (!isApiUser(value)) { throw new Error("User 타입이 아닙니다."); }}assertIsUser(response.data);// 이 아래부터 response.data는 User로 추론됨
방법 10. noImplicitAny 설정으로 any 차단하기
tsconfig.json 설정으로 암묵적 any를 컴파일러가 거부하게 만듭니다.
// 파일: tsconfig.json (권장 설정)
{ "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitReturns": true, "noUncheckedIndexedAccess": true }}
// 파일: src/strict-mode.ts// noImplicitAny가 없으면 통과, 있으면 에러function processItems(items) { // 에러: 'items' 매개변수는 암시적으로 'any' 형식이 있습니다. return items.map((i: any) => i.name);}// 올바른 작성function processItemsSafe(items: Array<{ name: string }>) { return items.map((i) => i.name);}
요약
| 상황 | any 대신 |
|---|---|
| 타입을 모르는 외부 데이터 | unknown + 타입 가드 |
| 다양한 타입을 처리하는 함수 | 제네릭 <T> |
| 동적 키 객체 | Record<K, V> |
| 상태/종류 표현 | 유니온 타입 |
| 타입 검사 + 리터럴 유지 | satisfies |
| 입력별 다른 반환 타입 | 함수 오버로드 |
| 기존 타입 변형 | Omit, Pick, Partial |
| 불가피한 단언 | as unknown as T |
| 레거시 코드 연동 | asserts 타입 가드 |
| 팀 전체 강제 | noImplicitAny: true |
any를 완전히 없애는 것이 목표가 아닙니다. 각 상황에서 더 정확하고 안전한 대안을 선택하는 것이 목표입니다. any를 쓸 때는 의도적으로, 범위를 좁혀서, 주석으로 이유를 남기는 것이 좋은 습관입니다.