1. 개요
any 와 unknown 은 둘 다 “어떤 타입이든 담을 수 있는 타입” 처럼 보인다. 실제로 값을 대입하는 방향으로는 둘 다 뭐든 받는다.
let a: any = 42
let u: unknown = 42그런데 그 값을 꺼내 쓰는 방향에서는 완전히 다르게 행동한다. 이 차이를 모르고 “귀찮으니까 any 쓰지 뭐” 로 넘어가면, TypeScript 를 쓰는 의미의 상당 부분을 그냥 버리는 셈이다.
2. 핵심 내용
2-1. any: 타입 체크를 끈다
any 타입 값은 어떤 프로퍼티에 접근하든, 어떤 메서드를 호출하든 컴파일러가 검사하지 않는다.
let a: any = 42
a.toUpperCase() // 컴파일 통과. 런타임에서 "a.toUpperCase is not a function"
a.foo.bar.baz() // 이것도 컴파일 통과any 는 이 값에 대해서는 타입 시스템을 아예 꺼버리는 것과 같다. 자바스크립트를 그대로 쓰는 것과 다를 게 없다. 심지어 any 는 전염된다. any 값을 다른 변수에 대입하면 그 변수도 any 가 되고, 함수 인자로 넘기면 그 함수 내부에서도 타입 정보가 사라진다.
2-2. unknown: 확인 전엔 아무것도 못 한다
unknown 도 뭐든 담을 수 있지만, 꺼내 쓰려면 먼저 무슨 타입인지 증명해야 한다.
let u: unknown = 42
u.toUpperCase() // Error: 개체가 'unknown' 형식입니다
u + 1 // Error: 'unknown' 형식에 산술 연산을 적용할 수 없습니다아무 프로퍼티 접근도, 연산도 그냥은 안 된다. 2편에서 본 좁히기를 거쳐야 한다.
function handle(u: unknown) {
if (typeof u === "string") {
console.log(u.toUpperCase()) // 이제 string 으로 좁혀져서 OK
}
}unknown 은 “지금은 무슨 타입인지 모른다” 를 정직하게 표현하는 타입이고, 그 무지를 컴파일러가 강제로 인정하게 만든다. 뭘 하려면 먼저 확인부터 하라는 게 unknown 의 계약이다.
2-3. 왜 unknown 이 있어야 했나
unknown 이 등장하기 전(TypeScript 3.0 이전)에는 “타입을 모르는 외부 값” 을 표현할 방법이 any 밖에 없었다. JSON.parse 의 반환 타입, 외부 API 응답, catch 절의 에러 객체가 전부 any 였다. 문제는 이게 타입 시스템의 구멍을 그대로 프로젝트 곳곳에 심는다는 것이다. JSON.parse(text) 의 결과를 별 확인 없이 .name 으로 접근하다가 실제로는 배열이 와서 런타임 에러가 나는 식이다. unknown 은 “값이 들어오는 건 막지 않지만, 쓰기 전엔 반드시 확인하게” 만들어서 이 구멍을 막는다.
TypeScript 4.4 부터는 catch 절의 에러 타입도 기본이 any 대신 unknown 으로 바뀌었다(useUnknownInCatchVariables). 지금은 catch (e) 를 쓰면 e 가 unknown 이라 바로 e.message 에 접근하면 에러가 나고, 아래처럼 확인을 거쳐야 한다.
try {
riskyCall()
} catch (e) {
if (e instanceof Error) {
console.log(e.message) // Error 로 좁혀졌으니 OK
} else {
console.log("알 수 없는 에러", e)
}
}옛날 코드에서 catch (e: any) 를 흔히 보는 이유가 이거다. any 시절 습관이 남아있거나, 확인 과정이 귀찮아서 명시적으로 any 로 되돌린 경우다.
2-4. 외부 입력은 unknown 으로 받는 게 기본이어야 한다
API 응답, 폼 입력, 로컬스토리지에서 읽은 값처럼 “내 코드 밖에서 온 값” 은 전부 unknown 으로 받는 게 원칙이다.
function parseUser(json: string): unknown {
return JSON.parse(json)
}
function isUser(v: unknown): v is { id: number; name: string } {
return (
typeof v === "object" &&
v !== null &&
typeof (v as any).id === "number" &&
typeof (v as any).name === "string"
)
}
const parsed = parseUser(rawJson)
if (isUser(parsed)) {
console.log(parsed.name) // 검증을 통과한 뒤에만 안전하게 접근
}JSON.parse 의 실제 시그니처는 any 를 반환하지만, 그걸 그대로 쓰지 않고 unknown 으로 감싸서 강제로 검증을 거치게 만드는 패턴이다. 검증 함수를 만드는 방식은 다음 편에서 좀 더 다룬다.
2-5. 정리하면
| any | unknown | |
|---|---|---|
| 아무 타입이나 대입 가능 | O | O |
| 확인 없이 프로퍼티 접근 | O (위험) | X (컴파일 에러) |
| 다른 타입 변수에 대입 | O, 전염됨 | X, 좁힌 뒤에만 가능 |
| 쓰는 상황 | 정말 마지막 수단 | 외부 입력, 아직 모르는 타입을 정직하게 표현 |
any 를 쓰는 순간 그 지점부터 TypeScript 는 자바스크립트가 된다. unknown 을 쓰면 “여기는 아직 타입을 모른다” 는 사실이 코드에 남고, 컴파일러가 그걸 확인 없이는 못 쓰게 강제한다. 대부분의 “일단 any” 는 unknown + 타입 가드로 바꿀 수 있다.
3. 마무리
요약
any는 타입 체크를 끈다. 확인 없이 아무 프로퍼티나 접근해도 컴파일은 통과하고 런타임에서 터진다.unknown은 뭐든 담을 수 있지만 꺼내 쓰려면 반드시 좁히기를 거쳐야 한다.JSON.parse결과, API 응답,catch의 에러 객체처럼 외부에서 오는 값은unknown으로 받는 게 기본이어야 한다. TypeScript 4.4 부터catch기본 타입도unknown이다.- “일단
any” 로 넘어가고 싶은 지점은 대부분unknown+ 타입 가드로 바꿀 수 있다.
다음은 7편이다. is 키워드로 런타임 검증과 컴파일 타임 타입을 잇는 법을 다룬다.