본문 바로가기

특수 타입

unknown·object·void·never·null·undefined의 허용 연산과 할당 관계를 구분해 불확실하거나 반환되지 않는 값을 표현합니다.

앞서 살펴본 기본 타입들은 타입스크립트의 가장 근간이 되는 데이터 형태를 정의했습니다.

이제는 좀 더 유연하고 강력하게 타입을 다룰 수 있도록 돕는 특수 타입들에 대해 알아보겠습니다.

이 특수 타입들은 특정 상황에서 매우 유용하게 활용되며, 타입스크립트의 진정한 강점을 경험하게 해 줄 것입니다.

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); // 1

push는 새 길이를 반환하지만 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로 설정하는 것이 좋습니다.


특수 타입은 강력하지만, 잘못 쓰면 타입 검사가 느슨해지거나 흐름 의미가 흐려질 수 있습니다.


실전에서는 특수 타입을 단독 문법으로 외우기보다, 값이 들어오고 검증되고 사용되는 흐름 속에서 어디에 배치할지 판단해야 합니다.

unknown 입력을 검증하고 내부 유니온으로 변환한 뒤 모든 경우를 처리합니다. 남은 값의 never 검사는 정적 누락 검사이며 런타임 validator와 역할이 다릅니다.

TYPE / RUNTIME / CONTRACT

입력 검증 뒤 닫힌 유니온의 누락을 검사한다

본문은 unknown의 typeof 좁히기와 never 반환을 설명합니다. 아래는 이를 이어 붙인 확장 설계입니다. guard 통과만으로 닫힌 도메인 유니온이 저절로 생기지는 않습니다.

입력 검증과 정적 누락 검사는 다른 경계검증→도메인변환→상태처리→never정적누락검사를연결하는5노드4간선개념흐름이다. 이절의formatScore는숫자검증예이며 닫힌유니온과누락검사의전체실습은다음절확장코드에서따른다. never가정상반환값을실행중생성한다는뜻이아니다.
unknown 입력모르는 외부 값
연결 관계unknown 입력 · 모르는 외부 값 → 값 · 필드 검증 · 실패 값은 거절
값 · 필드 검증실패 값은 거절
연결 관계값 · 필드 검증 · 실패 값은 거절 → 도메인 변환 · 닫힌 유니온 계약
도메인 변환닫힌 유니온 계약
연결 관계도메인 변환 · 닫힌 유니온 계약 → 각 상태 처리 · 판별 속성으로 좁히기
각 상태 처리판별 속성으로 좁히기
연결 관계각 상태 처리 · 판별 속성으로 좁히기 → 남은 값 = never · 정적 누락 검사
남은 값 = never정적 누락 검사
입력 검증과 정적 누락 검사는 다른 경계 · 모바일검증→도메인변환→상태처리→never정적누락검사를연결하는5노드4간선개념흐름이다. 이절의formatScore는숫자검증예이며 닫힌유니온과누락검사의전체실습은다음절확장코드에서따른다. never가정상반환값을실행중생성한다는뜻이아니다. 모바일에서도 노드와 연결 방향·독립 경로가 같다.
unknown 입력모르는 외부 값
연결 관계unknown 입력 · 모르는 외부 값 → 값 · 필드 검증 · 실패 값은 거절
값 · 필드 검증실패 값은 거절
연결 관계값 · 필드 검증 · 실패 값은 거절 → 도메인 변환 · 닫힌 유니온 계약
도메인 변환닫힌 유니온 계약
연결 관계도메인 변환 · 닫힌 유니온 계약 → 각 상태 처리 · 판별 속성으로 좁히기
각 상태 처리판별 속성으로 좁히기
연결 관계각 상태 처리 · 판별 속성으로 좁히기 → 남은 값 = never · 정적 누락 검사
남은 값 = never정적 누락 검사

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는 코드의 안전성을 한 단계 더 높여주는 중요한 도구임을 기억해주세요.