특수 타입
unknown·object·void·never·null·undefined의 허용 연산과 할당 관계를 구분해 불확실하거나 반환되지 않는 값을 표현합니다.
앞서 살펴본 기본 타입들은 타입스크립트의 가장 근간이 되는 데이터 형태를 정의했습니다.
이제는 좀 더 유연하고 강력하게 타입을 다룰 수 있도록 돕는 특수 타입들에 대해 알아보겠습니다.
이 특수 타입들은 특정 상황에서 매우 유용하게 활용되며, 타입스크립트의 진정한 강점을 경험하게 해 줄 것입니다.
TYPE / RUNTIME / CONTRACT
특수 타입은 값과 흐름의 의미로 선택한다
unknown·object·void·never·null·undefined를 안전성 순위로 나열하지 않습니다. 받는 값, 허용할 연산, 함수의 정상 반환, 결측 의미를 따로 확인합니다.
UNKNOWN / USE
모든 값 수신, 사용 전 좁히기
본문은 typeof string/number를 확인한 뒤 toUpperCase/toFixed를 호출합니다. object 분기는 null을 제외하지만 특정 속성의 모양까지 확인한 것은 아닙니다.
OBJECT / SHAPE
배열 · 함수도 비원시 값이다
object는 원시 타입을 제외하지만 name 같은 속성을 보장하지 않습니다. 구체적인 속성이 필요하면 객체 타입이나 interface를 사용합니다.
VOID / RESULT
정상 종료와 결과 미사용
본문 logMessage:void는 값을 돌려주지 않고 정상 종료합니다. ()=>void 콜백 계약에는 값을 반환하는 함수도 들어올 수 있으며 호출자는 그 결과를 사용하지 않습니다.
NEVER / CONTROL
정상적으로 돌아오는 값이 없다
throwError와 무한 루프는 never입니다. never는 다른 타입에 할당 가능하지만, throwError 호출은 예외를 던져 실제 할당할 값이 생기지 않습니다.
NULL / MEANING
없는 값을 계약에 명시한다
strictNullChecks=true의 nullableString:string|null은 null을 허용합니다. 본문의 느슨한 설정에서는 number에 null도 들어가지만 같은 동작을 strict 설정으로 확대하지 않습니다.
UNDEFINED / MEANING
생략과 undefined 쓰기는 설정도 확인
number|undefined는 undefined를 허용합니다. void에도 undefined가 허용됩니다. 선택적 속성의 생략과 명시적 undefined 쓰기는 exactOptionalPropertyTypes 등 실제 설정에 따라 구분합니다.
unknown은 사용 전 좁히기를 요구하고 object는 비원시 값을 표현합니다. void의 결과 계약과 never의 정상 반환 불가는 다르며 null·undefined는 설정에 맞게 표현합니다.
unknown (알 수 없음)
unknown 타입은 any와 비슷해 보이지만,
훨씬 더 타입 안전성(Type Safety)을 강조합니다.
any는 모든 값 할당과 연산을 허용해 타입 검사를 사실상 무력화합니다.
반면 unknown은 값 할당은 허용하되,
구체적인 타입을 요구하는 속성 접근이나 메서드 호출을 바로 허용하지 않습니다. typeof 검사나 null과의 비교처럼 타입을 확인하는 연산은 할 수 있습니다.
unknown 값을 사용하려면 먼저
그 값에 필요한 조건을 확인하고 타입 좁히기(Narrowing)를 합니다. as로 검사를 우회할 수도 있지만, 단언은 실제 값의 검증을 대신하지 않습니다.
이는 마치 내가 뭘 들고 있는지 모르니 함부로 만지지 마세요!라고 말하는 것과 같습니다.
let value: unknown;
value = 123; // unknown에 숫자 할당 가능
value = "hello"; // unknown에 문자열 할당 가능
value = true; // unknown에 불리언 할당 가능
value = { a: 1 }; // unknown에 객체 할당 가능
// 오류: 'value'의 형식이 'unknown'이므로 개체에서 'toUpperCase' 속성을 확인할 수 없습니다.
// value.toUpperCase();
// 오류: 'value'의 형식이 'unknown'이므로 개체에서 'toFixed' 속성을 확인할 수 없습니다.
// value.toFixed(2);
// unknown 타입의 값을 사용하려면 반드시 타입 검사를 통해 타입을 좁혀야 합니다.
if (typeof value === "string") {
console.log(value.toUpperCase()); // 이제 문자열 메서드를 사용할 수 있습니다.
} else if (typeof value === "number") {
console.log(value.toFixed(2)); // 이제 숫자 메서드를 사용할 수 있습니다.
} else if (typeof value === "object" && value !== null) {
// 객체인 경우 추가적인 속성 검사가 필요할 수 있습니다.
console.log(value);
}unknown은 API 응답처럼 어떤 형태의 데이터가 올지 확실하지 않을 때 유용합니다.
any보다 안전하게 미지의 데이터를 다룰 수 있도록 돕기 때문에, 가능한 한 any 대신 unknown을 사용하는 것이 좋습니다.
위 예제는 마지막에 { a: 1 }을 할당했으므로 객체 분기로만 들어갑니다. 같은 값이 모든 분기를 차례로 실행하는 것이 아닙니다. 숫자 표시가 목적이라면 숫자인지와 유한한 값인지를 따로 확인할 수 있습니다.
예시
function formatScore(input: unknown): string {
if (typeof input === "number" && Number.isFinite(input)) {
return input.toFixed(2);
}
return "숫자 점수 아님";
}
console.log(formatScore(3)); // 3.00
console.log(formatScore("3")); // 숫자 점수 아님typeof로 숫자 메서드를 사용할 수 있는 타입을 좁히고, Number.isFinite로 NaN과 무한대를 제외합니다. 숫자로 보이는 문자열을 자동 변환하는 함수는 아닙니다. 이 새 예제는 strict: true, target: "ES2020"을 기준으로 합니다.
직접 추적하기: formatScore(null), formatScore(NaN), formatScore(2.5)의 결과를 각각 적어보세요. input as number만 쓰는 변경이 같은 검증인지도 설명해 보세요.
조건별 반환값과 단언의 한계
앞의 두 호출은 숫자 점수 아님, 마지막은 2.50입니다. null은 첫 타입 검사에서, NaN은 유한성 검사에서 제외됩니다. 단언만 쓰면 실제 값은 바뀌지 않아 문자열이나 null을 숫자 메서드로 처리할 때 실패할 수 있습니다.
object (객체)
object 타입은 원시 타입(primitive types: number, bigint, string, boolean, symbol, null, undefined)이 아닌 모든 타입을 나타냅니다.
즉, 객체, 배열, 함수 등을 포함하는 더 넓은 개념의 "객체"를 의미합니다.
let obj: object;
obj = { name: "Alice", age: 30 }; // 객체
obj = [1, 2, 3]; // 배열도 객체의 일종
obj = function() { console.log("Hello"); }; // 함수도 객체의 일종
// 오류: 원시 타입은 object 타입에 할당될 수 없습니다.
// obj = 100;
// obj = "hello";
// obj = true;
// object 타입은 객체의 특정 속성에 바로 접근하는 것을 허용하지 않습니다.
// 오류: 'obj'의 형식이 'object'이므로 개체에서 'name' 속성을 확인할 수 없습니다.
// console.log(obj.name);object 타입은 매우 일반적인 타입이므로, 실제 코딩에서는 특정 객체 구조를 정의하는 객체 리터럴 타입(예: { name: string, age: number })이나 인터페이스(Interface), 타입 별칭(Type Alias)을 더 자주 사용하게 됩니다.
object 타입은 단순히 이 값은 원시 타입이 아니라고 명시할 때 유용합니다.
void와 never의 재조명 (함수 관점)
기본 타입에서 이미 void와 never를 다루었지만, 이 두 타입은 특히 함수의 반환 타입과 관련하여 중요한 의미를 가지므로 다시 한번 강조하고자 합니다.
-
void: 함수가 아무런 값도 반환하지 않을 때 사용됩니다.console.log()처럼 단순히 작업을 수행하고 끝나는 함수가 대표적입니다.undefined를 반환하는 함수도void타입으로 간주될 수 있습니다.function logMessage(message: string): void { console.log(message); // return undefined; // 명시적으로 undefined를 반환해도 void입니다. // return; // 이것도 void } logMessage("반환값이 없는 함수입니다."); // let result: string = logMessage("테스트"); // 오류: 'void' 형식은 'string' 형식에 할당될 수 없습니다. -
never: 함수가 절대로 값을 반환하지 않거나, 정상적으로 종료되지 않을 때 사용됩니다.이는 함수가 항상 예외를 던지거나, 무한 루프에 빠지는 경우를 의미합니다.
즉, 함수의 실행 흐름이 끝에 도달할 수 없음을 나타냅니다.
// 항상 예외를 발생시키는 함수 function throwError(message: string): never { throw new Error(message); } // 무한 루프에 빠지는 함수 function keepProcessing(): never { while (true) { // ... 계속해서 작업을 수행 } } // 이 함수는 never 타입을 반환하므로, 정상적인 값을 할당받을 수 없습니다. // let result: string = throwError("치명적인 오류 발생!"); // 타입 할당은 허용되지만 호출이 throw하므로 값은 할당되지 않습니다.never는string을 포함한 모든 타입에 할당할 수 있습니다. 다만throwError호출은 정상 반환하지 않으므로 실제로 할당할 값은 생기지 않습니다.이 예제의
void함수는 정상적으로 끝날 수 있고never함수는 정상적으로 돌아오지 않습니다. 별도로() => void콜백 계약은 반환값을 사용하지 않는다는 의미여서, 값을 반환하는 함수를 받을 수도 있습니다.
반환값을 쓰지 않는 콜백의 예시
const pending: string[] = [];
const recordReview: () => void = () => pending.push("검토");
recordReview();
console.log(pending.length); // 1push는 새 길이를 반환하지만 recordReview의 호출 계약은 그 반환값을 사용하지 않습니다. 실제 실행 결과가 항상 undefined라는 뜻은 아닙니다. 반면 function recordReview(): void { return 1; }처럼 함수 선언 자체에 반환 타입을 적으면 숫자를 명시적으로 반환할 수 없다고 진단합니다. 공식 함수 설명의 두 경우를 구별하세요.
직접 판단하기: 위 콜백을 두 번 호출하면 배열 길이는 얼마인가요? const count: number = recordReview()가 검사에 통과하는지도 따로 판단하세요.
작업 결과와 반환 계약 구별
배열 길이는 2입니다. 그러나 recordReview()의 정적 반환 타입은 void이므로 number 변수에 할당할 수 없습니다. 배열을 바꾼 작업 결과와 호출자가 사용할 반환값의 타입은 별개입니다.
null과 undefined의 특수성
다시 한번 null과 undefined를 언급하는 이유는, 이들이 타입스크립트의 strictNullChecks 컴파일 옵션과 결합될 때 가지는 중요한 의미 때문입니다. TypeScript 6.0부터 strict의 기본값은 true이며, strictNullChecks는 개별 설정으로 덮을 수 있습니다. 아래 두 예제는 프로젝트에 실제로 적용된 strictNullChecks 값으로 구분해서 읽습니다.
-
strictNullChecks: false(결측 검사를 끈 설정): 이 설정에서는null과undefined를number,string같은 타입에도 할당할 수 있게 됩니다. 값이 존재하지 않는 타입인never까지 할당을 허용한다는 뜻은 아닙니다. 즉,number타입 변수에null이나undefined를 할당하는 것이 허용됩니다. 이는 자바스크립트의 느슨한 특성을 반영하지만, 런타임에TypeError: Cannot read property of null (or undefined)같은 흔한 오류를 유발할 수 있습니다.// tsconfig.json 에 "strictNullChecks": false 일 때 let myNumber: number = 10; myNumber = null; // 허용됨 myNumber = undefined; // 허용됨 console.log(myNumber); // undefined -
strictNullChecks: true(권장 설정): 이 설정에서는null과undefined를 각각 허용하는 타입이나 유니온을 명시해야 합니다. 두 값은any·unknown에도 할당할 수 있으며,void에는undefined가 허용되지만null은 허용되지 않습니다.다른 타입에
null또는undefined를 할당하려고 하면 컴파일 시 오류가 발생합니다.// tsconfig.json 에 "strictNullChecks": true 일 때 let myNumber: number = 10; // myNumber = null; // 오류: 'null' 형식은 'number' 형식에 할당될 수 없습니다. // myNumber = undefined; // 오류: 'undefined' 형식은 'number' 형식에 할당될 수 없습니다. // 만약 null 또는 undefined를 허용하고 싶다면, 유니온 타입을 사용해야 합니다. let nullableString: string | null = "Hello"; nullableString = null; // 허용됨 let optionalNumber: number | undefined = 50; optionalNumber = undefined; // 허용됨이 책에서는
strictNullChecks: true를 활성화하는 것을 강력히 권장합니다.이 설정을 통해 타입스크립트의 안전성을 극대화하고, 런타임에 발생할 수 있는
null관련 오류를 사전에 방지할 수 있기 때문입니다.프로젝트에 적용되는
strictNullChecks값을 확인하고, 꺼져 있다면true로 설정하는 것이 좋습니다.
특수 타입은 강력하지만, 잘못 쓰면 타입 검사가 느슨해지거나 흐름 의미가 흐려질 수 있습니다.
실전에서는 특수 타입을 단독 문법으로 외우기보다, 값이 들어오고 검증되고 사용되는 흐름 속에서 어디에 배치할지 판단해야 합니다.
TYPE / RUNTIME / CONTRACT
입력 검증 뒤 닫힌 유니온의 누락을 검사한다
본문은 unknown의 typeof 좁히기와 never 반환을 설명합니다. 아래는 이를 이어 붙인 확장 설계입니다. guard 통과만으로 닫힌 도메인 유니온이 저절로 생기지는 않습니다.
RUNTIME / VALIDATION
모양과 필요한 값 조건을 확인
typeof와 필드 검사로 실제 입력을 확인합니다. 허용할 값의 범위까지 필요한 경우 함께 검사하고 실패를 거절합니다. 타입 주석이나 단언이 validator를 생성하지 않습니다.
DESIGN / CLOSED UNION
검증된 값을 도메인 계약으로 만든다
검증 결과를 닫힌 유니온의 하나로 변환합니다. 단순 object 검사만으로 특정 kind와 필드의 관계가 보장되는 것은 아닙니다.
STATIC / EXHAUSTIVENESS
모든 경우를 처리한 나머지만 never
닫힌 유니온에서 각 경우를 제거한 뒤 남은 값을 never에 할당합니다. 새 경우를 추가하고 처리하지 않으면 정적 진단이 발생합니다. 모든 성공 분기가 never 값을 반환하는 것은 아닙니다.
LESSON / EXTENSION
본문의 좁히기와 확장 설계를 구분
이 절의 코드는 unknown typeof 분기, object, void/never, null 설정 예입니다. 여기의 도메인 변환과 판별 유니온 전체 validator는 학습을 연결하는 추가 설계입니다. 다음 유니온 절의 확장 코드에서 실제 검증·분기·누락 검사를 연결해 실행합니다.
unknown 입력을 검증하고 내부 유니온으로 변환한 뒤 모든 경우를 처리합니다. 남은 값의 never 검사는 정적 누락 검사이며 런타임 validator와 역할이 다릅니다.
이렇게 unknown, object, 그리고 void, never, null, undefined의 심화된 사용법까지 알아보았습니다.
특히 unknown과 strictNullChecks는 코드의 안전성을 한 단계 더 높여주는 중요한 도구임을 기억해주세요.