세 구역 이야기
어떤 건물에 세 구역이 있다고 상상해보세요.
첫 번째 구역은 무법지대입니다. 신분증 확인 없이 누구나 들어올 수 있고, 무엇이든 할 수 있습니다. 편리하지만 위험합니다. 이것이 any입니다.
두 번째 구역은 신원확인 필수 구역입니다. 들어올 수는 있지만, 뭔가를 하려면 신분 확인을 먼저 해야 합니다. 번거롭지만 안전합니다. 이것이 unknown입니다.
세 번째 구역은 존재할 수 없는 곳입니다. 지도에는 표시되어 있지만 실제로는 아무도 도달할 수 없습니다. 이것이 never입니다.
any — 무법지대
any는 TypeScript의 타입 검사를 완전히 끄는 타입입니다.
let data: any = "문자열";data = 42; // OKdata = true; // OKdata = { x: 1 }; // OKdata = [1, 2, 3]; // OK
어떤 값이든 담을 수 있습니다. 문제는 이게 끝이 아닙니다.
let data: any = "김민수";const upper = data.toUpperCase(); // OK — 하지만 런타임에서 확인해야 함const num = data * 2; // OK — TypeScript는 에러 안 냄data.nonExistentMethod(); // OK — TypeScript는 에러 안 냄
any 타입에는 아무 메서드나 호출할 수 있고, 어떤 연산이든 할 수 있습니다. TypeScript가 검사를 포기했기 때문입니다. 그러면 에러는 코드를 실행할 때 납니다. TypeScript를 쓰는 이유가 사라집니다.
any가 퍼지면 더 무서워집니다.
let data: any = "김민수";const result = data * 2; // result의 타입도 any가 됨const final = result + 1; // final의 타입도 any가 됨
any에서 나온 값도 any가 됩니다. 한 곳에서 시작된 any가 코드 전체로 퍼져나갑니다.
언제 any를 써도 될까요. 서드파티 라이브러리가 타입을 제공하지 않거나, 복잡한 타입 문제를 일시적으로 넘어가야 할 때입니다. 하지만 임시방편으로 사용하고, 가능한 빨리 구체적인 타입으로 교체해야 합니다.
unknown — 신원확인 필수 구역
unknown은 any처럼 어떤 값이든 담을 수 있지만, 사용하기 전에 타입을 확인해야 합니다.
let data: unknown = "김민수";// 타입 확인 없이 사용하면 에러const upper = data.toUpperCase(); // 에러! Object is of type 'unknown'.const num = data * 2; // 에러!
any와 달리 unknown은 아무 작업도 허용하지 않습니다. 반드시 타입을 좁혀야 합니다.
let data: unknown = "김민수";// 타입 확인 후 사용if (typeof data === "string") { const upper = data.toUpperCase(); // OK — 이 블록 안에서 data는 string}if (typeof data === "number") { const doubled = data * 2; // OK — 이 블록 안에서 data는 number}
typeof로 타입을 확인하면, 해당 블록 안에서는 TypeScript가 구체적인 타입으로 인식합니다. 이것을 타입 좁히기(Type Narrowing)라고 합니다. 자세한 내용은 PART 05에서 다룹니다.
unknown은 외부에서 들어오는 데이터를 다룰 때 유용합니다. API 응답, JSON.parse() 결과, 에러 객체 등이 대표적입니다.
try { // 무언가를 시도} catch (error: unknown) { if (error instanceof Error) { console.log(error.message); // OK }}
에러 객체는 어떤 타입인지 모르므로 unknown으로 처리한 뒤, 타입을 확인하고 사용하는 것이 안전합니다.
never — 존재할 수 없는 곳
never는 "절대 발생할 수 없는 타입"입니다. 값이 존재하지 않는 상태를 표현합니다.
직접 쓰기보다 자연스럽게 나오는 경우가 많습니다.
모든 경우를 처리한 뒤 남은 타입
type Shape = "circle" | "square" | "triangle";function getArea(shape: Shape): number { if (shape === "circle") { return Math.PI * 5 * 5; } else if (shape === "square") { return 5 * 5; } else if (shape === "triangle") { return (5 * 5) / 2; } else { // 여기서 shape의 타입은 never // 모든 경우를 처리했으므로 이 코드에 도달할 수 없음 const exhausted: never = shape; throw new Error(`처리되지 않은 도형: ${exhausted}`); }}
Shape의 모든 경우를 처리했으므로 else 블록에서 shape는 never가 됩니다. 나중에 Shape에 새로운 도형을 추가하면 이 패턴이 에러를 내주어 처리를 빠트리지 않도록 도와줍니다.
절대 반환하지 않는 함수
function throwError(message: string): never { throw new Error(message);}function infiniteLoop(): never { while (true) {}}
함수가 항상 예외를 던지거나 무한 루프라면 반환값이 없습니다. 이때 반환 타입이 never입니다. void와 다릅니다. void는 "값 없이 반환한다"이고, never는 "반환 자체가 일어나지 않는다"입니다.
세 타입 비교표
| 타입 | 담을 수 있는 값 | 사용 전 확인 필요 | 사용 예 |
|---|---|---|---|
any |
모든 값 | 필요 없음 (위험) | 임시 회피, 레거시 코드 |
unknown |
모든 값 | 반드시 필요 | API 응답, JSON 파싱, 에러 처리 |
never |
없음 | 해당 없음 | 철저한 분기 처리, 무한 루프, 예외 던지기 |
any 대신 unknown을 써야 하는 이유
// any — 위험function processAny(data: any) { return data.toUpperCase(); // 런타임에 에러날 수 있음}// unknown — 안전function processUnknown(data: unknown) { if (typeof data === "string") { return data.toUpperCase(); // 안전하게 처리 } return "";}
타입을 모르는 값이 들어올 때 any를 쓰면 TypeScript의 보호막이 사라집니다. unknown을 쓰면 타입 확인을 강제하므로, 잘못된 접근을 컴파일 시점에 잡아줍니다.
다음 챕터에서는 TypeScript 코드를 작성하다 처음 만나는 에러 메시지들을 어떻게 읽는지, 당황하지 않는 법을 살펴보겠습니다.