any와 unknown은 둘 다 모든 값을 받습니다. 차이는 그다음에 그 값으로 무엇을 할 수 있느냐입니다. any는 무엇이든 하게 두고 아무것도 검사하지 않지만, unknown은 값이 무엇인지 증명하기 전까지 거의 아무것도 하지 못하게 합니다.
@ts-expect-error 줄이 없으면 u.toUpperCase()는 컴파일 오류 TS18046입니다. typeof 검사가 u를 string으로 좁히고, 그 블록 안에서는 모든 문자열 메서드를 쓸 수 있습니다.
any와 unknown 한눈에 비교
any | unknown | |
|---|---|---|
| 모든 값을 받음 | 예 | 예 |
string, number 등에 대입 | 예, 검사 없음 | 아니요(TS2322) |
| 속성 읽기, 메서드 호출 | 예, 검사 없음 | 아니요(TS18046) |
| 함수로 호출 | 예 | 아니요 |
산술과 비교(x * 2, x + 1, x < 5) | 예 | 아니요(TS18046) |
| 쓰기 전에 검사 필요 | 아니요 | 예(typeof, instanceof, in, 타입 가드) |
| 타입 검사에 미치는 영향 | 그 값과 그 값이 닿는 모든 것의 검사가 꺼짐 | 유지됨 |
타입 이론으로 말하면 unknown은 최상위 타입(top type)입니다. 모든 타입을 unknown에 대입할 수 있지만, unknown은 unknown과 any에만 대입할 수 있습니다. any는 양방향으로 대입할 수 있는 비상구로, never를 제외한 모든 것에 대입됩니다.
any는 타입 검사를 끈다
any 타입의 값은 맹목적으로 신뢰됩니다. 컴파일러는 오타, 잘못된 타입, 없는 속성을 받아들이고, 실수는 대신 런타임에 드러납니다.
출력이 문제를 보여 줍니다. number로 표기된 변수에 문자열이 들어 있고, 마지막 줄은 Cannot read properties of undefined (reading 'city')를 던집니다. any는 퍼지기도 합니다. user.name은 any이므로 그것으로 계산한 값도 모두 any가 되고, 타입 없는 값 하나가 들어온 곳에서 멀리 떨어진 곳의 검사까지 끌 수 있습니다.
unknown은 먼저 확인하게 만든다
unknown이면 코드가 값을 좁히기 전까지 컴파일러가 모든 연산을 거부합니다. 좁히기에는 평범한 JavaScript 검사를 쓰고, 각 분기 안에서 값은 확인된 타입을 갖습니다.
typeof null이 "object"이므로 value === null 검사가 객체 검사보다 먼저 와야 합니다. "id" in value 뒤에 TypeScript는 객체에 id 속성이 있다는 것을 알지만, 무엇이 들어 있는지 알려 주는 것이 없으므로 그 타입은 unknown입니다. String()이 명시적으로 텍스트로 바꿉니다. 숫자로 쓰려면 먼저 typeof로 확인해야 합니다.
단언 value as string으로 검사를 건너뛸 수도 있고, 컴파일러는 받아들입니다. 하지만 그것은 런타임 검사가 뒷받침하지 않는 약속이므로 실제 검사를 쓰세요. 타입 단언을 참고하세요.
unknown으로 JSON 검증하기
JSON.parse는 any를 반환하도록 선언되어 있어서, 그 결과는 조용히 검사를 끕니다. 결과를 unknown으로 표기하고, 나머지 코드가 믿기 전에 모양을 확인하는 타입 가드를 작성하세요.
첫 입력은 dark at 14px를 출력하고, 두 번째 입력은 fontSize가 없어서 거부됩니다. any였다면 두 번째 입력은 fontSize가 undefined인 Settings로 그대로 흘러갔을 것입니다. 스키마가 크다면 검증 라이브러리가 같은 일을 하고 타입도 대신 끌어내 줍니다.
noImplicitAny
any는 누군가 직접 써야만 생기는 것이 아닙니다. 표기도 없고 추론할 문맥도 없는 매개변수도 any가 됩니다. strict가 켜는 noImplicitAny 옵션은 이를 오류로 보고합니다.
index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.
해결책은 표기, function double(x: number)입니다. x: any라고 명시적으로 써도 컴파일되는데, 그것이 핵심입니다. any가 코드에 보이는 채로 남아 검색하고 리뷰할 수 있습니다.
여전히 any가 끼어드는 곳
strict에서도 다음은 코드에 그 단어가 없는데도 any를 만듭니다.
| 출처 | 얻는 타입 | 할 일 |
|---|---|---|
JSON.parse(text) | any | unknown으로 표기한 뒤 검증 |
브라우저(DOM) 타입의 response.json() | Promise<any> | JSON.parse와 같음(Node 자체의 fetch 타입은 이미 Promise<unknown>을 반환) |
| 타입 정의가 없는 패키지 | import가 any(선언을 추가하지 않으면 오류) | @types/...를 설치하거나 .d.ts 작성 |
value as any | any | 타입 가드나 정확한 단언 사용 |
useUnknownInCatchVariables가 꺼진 catch (e) | any | strict가 unknown으로 만들므로 그대로 유지 |
Error 객체만이 아니라 무엇이든 던질 수 있으므로 strict에서 catch 변수는 unknown입니다. e.message를 읽기 전에 e instanceof Error로 좁히세요.
any를 써도 괜찮을 때
any가 금지된 것은 아니지만, 하나하나가 컴파일러가 더 이상 보호하지 않는 지점입니다. 합리적인 용도:
- JavaScript 코드베이스를 옮기는 중에 아직 타입이 없는 곳을
any로 표시할 때. - 타입 시스템으로 잘 표현할 수 없는 코드를 작게 유지하고 타입이 있는 함수 시그니처 뒤에 숨길 때.
- 일부러 잘못된 데이터를 넘기는 테스트 코드.
그 밖의 모든 경우에는 unknown이 같은 "이 타입을 모른다"는 상황을 다루면서 검사를 유지합니다. 많은 팀이 @typescript-eslint/no-explicit-any 린트 규칙으로 이를 강제합니다. 관련 타입인 Record<string, unknown>은 "값을 모르는 어떤 객체"를 나타낼 때 흔히 씁니다.
자주 묻는 질문
TypeScript에서 any와 unknown의 차이는 무엇인가요?
둘 다 어떤 값이든 받습니다. any는 그 값으로 무엇이든 할 수 있게 하고(속성 읽기, 호출, number에 대입) 컴파일러는 아무것도 검사하지 않습니다. unknown은 typeof x === "string" 같은 검사로 좁히기 전까지는 거의 아무것도 할 수 없게 합니다. unknown은 타입 검사를 유지하므로 더 안전한 선택입니다.
언제 any 대신 unknown을 써야 하나요?
컴파일 시점에 값의 타입을 알 수 없을 때마다 쓰세요. 파싱한 JSON, 네트워크 응답 데이터, catch로 잡은 오류, 검증 함수의 입력 등입니다. unknown으로 타입을 지정하고 좁히세요. any는 단기적인 마이그레이션 작업이나 타입 시스템으로 표현할 수 없는 코드에만 쓰세요.
"Object is of type 'unknown'"은 무슨 뜻인가요?
오류 TS18046('x' is of type 'unknown')과 TS2571(Object is of type 'unknown', load().id처럼 값이 단순한 이름이 아닐 때 쓰임)은 unknown 값을 특정 타입인 것처럼, 예를 들어 속성을 읽거나 메서드를 호출하는 식으로 썼다는 뜻입니다. 먼저 타입을 확인하고(typeof, instanceof, Array.isArray, in, 또는 타입 가드 함수) 좁혀진 분기 안에서 값을 쓰세요.
JSON.parse는 왜 any를 반환하나요?
문자열에 무엇이 들어 있는지 컴파일러가 알 수 없으므로, 표준 라이브러리의 선언이 parse(text: string, ...): any로 되어 있기 때문입니다. 그 결과는 닿는 모든 것의 검사를 조용히 끕니다. 대신 const data: unknown = JSON.parse(text)로 표기하고, 쓰기 전에 검증하세요.
noImplicitAny란 무엇인가요?
strict에 포함된 컴파일러 옵션으로, 표기도 없고 추론할 근거도 없어서 선언이 조용히 any 타입을 받게 될 때 오류를 보고합니다. 매개변수는 TS7006, 타입을 알아낼 수 없는 변수는 TS7005나 TS7034입니다. 아무도 쓰지 않았는데 any가 생기는 것을 막아 줍니다. 명시적으로 쓴 any는 여전히 허용됩니다.