Exclude<T, U>는 유니언 타입에서 멤버를 제거하고, Extract<T, U>는 일치하는 멤버만 남기며, NonNullable<T>는 null과 undefined를 제거합니다. 셋 다 유니언을 걸러낼 뿐 객체 속성은 건드리지 않습니다.
다른 모든 유틸리티 타입처럼 이들도 컴파일 시점에만 존재합니다. 마지막 줄은 컴파일러가 거부하며, 실행되는 JavaScript는 Visible이 무엇인지 전혀 모릅니다.
Exclude, Extract, NonNullable 한눈에 보기
| 타입 | 남기는 것 | 예시 | 결과 |
|---|---|---|---|
Exclude<T, U> | U에 대입할 수 없는 T의 멤버 | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
Extract<T, U> | U에 대입할 수 있는 T의 멤버 | Extract<"a" | "b" | "c", "a" | "z"> | "a" |
NonNullable<T> | null과 undefined를 뺀 전부 | NonNullable<string | null | undefined> | string |
핵심 표현은 "U에 대입 가능한"입니다. U가 정확한 멤버 목록일 필요는 없습니다. 여러 멤버가 들어맞는 더 넓은 타입이어도 됩니다.
모양으로 유니언 멤버 고르기
구별된 유니언에서 부분적인 모양을 넘긴 Extract는 그 모양을 가진 변형을 뽑아냅니다. 직접 작성하지 않은 유니언의 변형 하나에 이름을 붙이는 흔한 방법입니다.
모양 { type: "click" }은 click 변형만의 상위 타입이므로 Extract는 그것만 남깁니다. 여러 변형에 들어맞는 모양이라면(예를 들어 두 변형이 { x: number }를 가졌다면) 모두 남깁니다.
NonNullable
NonNullable<T>는 !== null && !== undefined 검사의 타입 버전입니다. 선택적인 것에서 타입을 파생할 때 등장합니다.
User["manager"]는 { name: string } | null | undefined이고(?가 undefined를 더합니다), NonNullable은 둘 다 벗겨 냅니다. TypeScript 5.5부터는 컴파일러가 그 타입 서술어를 스스로 추론하므로 raw.filter((s) => s != null)은 이미 string[]으로 타입 지정됩니다. 명시적인 s is NonNullable<typeof s>는 의도를 드러내 주고 이전 버전에서도 동작합니다. 정의는 type NonNullable<T> = T & {}입니다. 빈 객체 타입 {}는 null과 undefined를 뺀 모든 값을 받으므로, 이것과 교차시키면 정확히 그 둘만 제거됩니다.
만들어진 원리
Exclude와 Extract는 표준 라이브러리에 있는 한 줄짜리 조건부 타입입니다.
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
T가 유니언이면 조건부 타입은 각 멤버에 따로 적용되고(분배), never 멤버는 결과에서 사라집니다. 그래서 Exclude<"a" | "b", "a">는 ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b"), 즉 never | "b", 즉 "b"가 됩니다. 분배는 조건부 타입 페이지에서 자세히 다룹니다.
Exclude vs Omit, Extract vs Pick
두 쌍 모두 무언가를 "제거"하거나 "남기기" 때문에 이름이 헷갈리기 쉽습니다. 차이는 무엇에 동작하느냐입니다.
| 대상 | 제거하거나 남기는 것 | 예시 | |
|---|---|---|---|
Exclude<T, U> | 유니언 | 유니언 멤버 | Exclude<"a" | "b", "a"> → "b" |
Extract<T, U> | 유니언 | 유니언 멤버 | Extract<"a" | "b", "a"> → "a" |
Omit<T, K> | 객체 타입 | 속성 | Omit<User, "password"> |
Pick<T, K> | 객체 타입 | 속성 | Pick<User, "name"> |
객체 타입에 Exclude를 쓰는 것은 흔한 실수이며, 조용히 실패합니다.
Account는 "password"에 대입할 수 없으므로 Exclude는 그것을 통째로 남깁니다. Omit 자체는 키에 적용한 Exclude로 만들어져 있습니다. Omit<T, K>는 Pick<T, Exclude<keyof T, K>>입니다. 자세한 내용은 Omit을 보세요.
더 엄격한 Exclude
Exclude의 두 번째 매개변수는 어떤 타입이든 받으므로, 오타는 아무것도 제거하지 않고 오류도 내지 않습니다. 제약을 추가하면 컴파일러가 제외하려는 것이 정말 유니언에 있는지 검사합니다.
index.ts(7,37): error TS2344: Type '"drafft"' does not satisfy the constraint 'Status'.
철자를 고치면 컴파일되고, Strict는 "published" | "archived"와 같아집니다. 같은 방법이 Extract와 엄격한 Omit(K extends keyof T)에도 통합니다.
자주 묻는 질문
TypeScript에서 Exclude는 무엇을 하나요?
Exclude<T, U>는 유니언 T에서 U에 대입 가능한 멤버를 모두 제거합니다. Exclude<"a" | "b" | "c", "a">는 "b" | "c"입니다. 여러 멤버를 제거하려면 U로 유니언을 넘기세요: Exclude<T, "a" | "b">.
Exclude와 Extract의 차이는 무엇인가요?
서로 반대입니다. Exclude<T, U>는 T의 멤버 중 U에 대입할 수 없는 것을 남기고, Extract<T, U>는 대입할 수 있는 것만 남깁니다. 같은 T와 U에 대해 두 결과를 합치면 T가 됩니다.
Exclude와 Omit의 차이는 무엇인가요?
Exclude는 유니언 타입에 동작하며 유니언 멤버를 제거합니다. Omit은 객체 타입에 동작하며 속성을 제거합니다. Exclude<User, "password">는 쓸모 있는 일을 하지 않고(User를 반환합니다), Omit<User, "password">는 그 속성이 없는 객체 타입을 줍니다. Omit은 Exclude로 만들어져 있어, keyof T에서 키를 제외합니다.
TypeScript에서 NonNullable은 무엇을 하나요?
NonNullable<T>는 타입에서 null과 undefined를 제거합니다. NonNullable<string | null | undefined>는 string입니다. {}가 null과 undefined를 뺀 모든 값을 받으므로 T & {}로 정의되어 있습니다.
존재하지 않는 멤버를 넘겨도 Exclude가 오류를 내지 않는 이유는 무엇인가요?
두 번째 매개변수에 제약이 없어서 어떤 타입이든 받기 때문에, Exclude<Status, "drafft"> 같은 오타는 조용히 아무것도 제거하지 않습니다. 더 엄격한 버전 type StrictExclude<T, U extends T> = Exclude<T, U>를 정의하면 컴파일러가 U가 T의 일부인지 검사합니다.