🔥 any / unknown / never: 셋의 구분
강의 목차

한 줄이 너무 쉽게 통과했다
나는 어제 작은 예제를 적다가 const payload: any = JSON.parse(raw) 한 줄에서 멈췄다. 바로 아래에서 payload.user.name.toUpperCase() 와 payload.createdAt.getTime() 를 적었는데 편집기가 조용했다. 두 번째 호출은 런타임에서 깨질 수 있는데도 타입 검사는 아무 말도 하지 않았다. 그제야 any 가 다음 연산까지 같이 느슨하게 만든다는 걸 봤다.
앞에서 다룬 추론과 리터럴 타입을 다룬 편 은 값을 더 정확히 적는 쪽이었다. 오늘 다루는 셋은 검사를 끄거나, 사용 전에 멈추게 하거나, 값이 아예 남지 않은 상태를 가리킨다.

any 는 검사 구멍을 넓힌다
any 는 한 값에서 끝나지 않고 그 값을 따라가는 연산 전체를 느슨하게 만든다. any 로 받은 값에서 꺼낸 속성도 다시 any 가 되고, 그 값에 메서드를 붙여도 컴파일러가 따지지 않는다. 그래서 오류는 선언한 줄보다 몇 줄 아래에서 뒤늦게 나온다. 나는 이 특성 때문에 any 를 오래 남겨 두지 않는다.
// file: any-spreads.ts
export {};
const payload: any = {
user: { name: "mejoo" },
createdAt: new Date("2026-05-08T00:00:00Z"),
};
const userName = payload.user.name;
const createdAt = payload.createdAt;
const upper = userName.toUpperCase();
const milliseconds = createdAt.getTime();
console.log(upper, milliseconds);
// function greet(name) {
// return name.toUpperCase();
// }// file: any-spreads.ts
export {};
const payload: any = {
user: { name: "mejoo" },
createdAt: new Date("2026-05-08T00:00:00Z"),
};
const userName = payload.user.name;
const createdAt = payload.createdAt;
const upper = userName.toUpperCase();
const milliseconds = createdAt.getTime();
console.log(upper, milliseconds);
// function greet(name) {
// return name.toUpperCase();
// }Parameter 'name' implicitly has an 'any' type.Parameter 'name' implicitly has an 'any' type.TS 6.0의 strict 기본 설정에서는 noImplicitAny 가 켜져 있어서, 매개변수 타입을 비워 두면 곧바로 멈춘다. 반면 명시적인 any 는 그 진단 대상이 아니라 통과한다. 그래서 그 값에 대한 이후 타입 검사가 사실상 사라진다. 편집기가 조용한 이유는 안전해서가 아니라, 그 값을 따라가는 검사가 사라졌기 때문이다.


unknown 은 사용 전에 확인을 요구한다
unknown 은 일단 받되, 바로 쓰지 못하게 막는 타입이다. useUnknownInCatchVariables 기본값 덕분에 strict 환경에서 catch 변수의 타입은 자동으로 unknown 이다. 반면 JSON.parse 의 반환 타입은 여전히 any 다. 그래서 외부 입력은 받자마자 unknown 으로 받아 두는 편이 더 안전하다. 컴파일러는 그 값을 문자열인지, 객체인지, 내가 기대한 구조인지 먼저 확인하라고 요구한다.
// file: unknown-narrowing.ts
export {};
type User = {
id: number;
name: string;
};
function isUser(v: unknown): v is User {
return (
typeof v === "object" &&
v !== null &&
"id" in v &&
"name" in v &&
typeof v.id === "number" &&
typeof v.name === "string"
);
}
function printValue(value: unknown): void {
// console.log(value.toUpperCase());
if (typeof value === "string") {
console.log(value.toUpperCase());
return;
}
if (value instanceof Error) {
console.log(value.message);
return;
}
if (isUser(value)) {
console.log(`${value.id}:${value.name}`);
}
}// file: unknown-narrowing.ts
export {};
type User = {
id: number;
name: string;
};
function isUser(v: unknown): v is User {
return (
typeof v === "object" &&
v !== null &&
"id" in v &&
"name" in v &&
typeof v.id === "number" &&
typeof v.name === "string"
);
}
function printValue(value: unknown): void {
// console.log(value.toUpperCase());
if (typeof value === "string") {
console.log(value.toUpperCase());
return;
}
if (value instanceof Error) {
console.log(value.message);
return;
}
if (isUser(value)) {
console.log(`${value.id}:${value.name}`);
}
}'value' is of type 'unknown'.'value' is of type 'unknown'.typeof, instanceof, "id" in v, 사용자 정의 타입 가드는 모두 타입을 더 구체적으로 좁히는 검사다. 막연한 입력은 이렇게 좁힌 뒤에만 연산할 수 있다. 추론을 다룬 편 에서 컴파일러가 타입을 채워 넣었다면, 여기서는 내가 필요한 정보를 먼저 밝혀야 한다.

never 는 빠진 분기를 드러낸다
never 는 모든 경우를 다 처리했는지 확인하는 마지막 분기 검사에서 쓰는 타입이다. 유니온을 switch 로 나눴을 때 남은 경우가 없다면 default 의 변수는 never 여야 한다. 값에서 하던 구분을 마지막 분기 확인이 그대로 이어 받는다.
// file: never-exhaustive.ts
export {};
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; size: number };
// type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square;
// type Shape = Circle | Square | Triangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default: {
const _exhaustive: never = shape;
return _exhaustive;
}
}
}// file: never-exhaustive.ts
export {};
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; size: number };
// type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square;
// type Shape = Circle | Square | Triangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default: {
const _exhaustive: never = shape;
return _exhaustive;
}
}
}Type 'Triangle' is not assignable to type 'never'.Type 'Triangle' is not assignable to type 'never'.지금 코드에서는 circle 과 square 를 모두 처리했으므로 default 에 남는 값이 없다. 그래서 shape 는 never 여야 한다. 그런데 주석의 Triangle 을 유니온에 추가하면 마지막 줄이 곧바로 실패한다. 빠진 분기를 런타임보다 먼저 잡는 이유가 이 패턴에 있다.

never 는 값이 없는 타입이다
never 는 다른 모든 타입에 대입할 수 있지만, 반대로는 어떤 값도 받을 수 없다. 그래서 throw 만 하는 함수의 반환 타입으로 적을 수 있다. 또 string | never 같은 유니온에서는 결과에 남지 않는다. 앞에서 본 원시 타입이나 리터럴 타입과 달리, 여기서는 값의 종류가 아니라 값이 없다는 사실을 적는다.
// file: never-bottom.ts
export {};
function fail(message: string): never {
throw new Error(message);
}
function requireToken(token: string | null): string {
if (token === null) {
return fail("token is missing");
}
return token;
}
const token = requireToken("abc");
console.log(token);
function describeUnreachable(value: never): string {
const asString: string = value;
return asString;
}
void describeUnreachable;
// const badNever: never = "text";// file: never-bottom.ts
export {};
function fail(message: string): never {
throw new Error(message);
}
function requireToken(token: string | null): string {
if (token === null) {
return fail("token is missing");
}
return token;
}
const token = requireToken("abc");
console.log(token);
function describeUnreachable(value: never): string {
const asString: string = value;
return asString;
}
void describeUnreachable;
// const badNever: never = "text";Type '"text"' is not assignable to type 'never'.Type '"text"' is not assignable to type 'never'.describeUnreachable 안의 const asString: string = value 가 통과하는 이유는 never 가 모든 타입의 부분집합이기 때문이다. 반대 방향, 즉 const badNever: never = "text" 는 컴파일러가 막는다. 이 차이를 알고 나면 throw 로 끝나는 함수와 빠진 분기 확인이 왜 모두 never 로 모이는지 한눈에 보인다.
마지막 메모
나는 요즘 외부 경계에서 any 를 보면 먼저 의심한다. 정말로 검사를 꺼야 하는 줄인지, 아니면 unknown 으로 받아 좁혀야 하는 줄인지부터 다시 읽는다.
never 를 볼 때도 먼저 위치를 확인한다. default 의 _exhaustive 인지, throw 만 하는 함수의 반환 타입인지에 따라 같은 단어가 가리키는 실패 지점이 다르다.











