iBetter Books
수정

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를 쓸 때는 의도적으로, 범위를 좁혀서, 주석으로 이유를 남기는 것이 좋은 습관입니다.