Exclude<T, U> remove membros de um union type, Extract<T, U> mantém só os membros que combinam, e NonNullable<T> remove null e undefined. Os três filtram unions; eles não mexem em propriedades de objetos.
Como todo utility type, eles existem só em tempo de compilação. A última linha é rejeitada pelo compilador; o JavaScript que roda não faz ideia do que é Visible.
Exclude, Extract e NonNullable em resumo
| Tipo | Mantém | Exemplo | Resultado |
|---|---|---|---|
Exclude<T, U> | membros de T não atribuíveis a U | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
Extract<T, U> | membros de T atribuíveis a U | Extract<"a" | "b" | "c", "a" | "z"> | "a" |
NonNullable<T> | tudo exceto null e undefined | NonNullable<string | null | undefined> | string |
"Atribuível a U" é a expressão-chave. U não precisa ser uma lista de membros exatos; pode ser um tipo mais amplo em que vários membros se encaixam:
Selecionando membros de uma union pelo formato
Em uma discriminated union, Extract com um formato parcial tira as variantes que têm esse formato. Esse é o jeito habitual de dar nome a uma variante de uma union que você não escreveu:
O formato { type: "click" } é supertipo só da variante click, então é essa que Extract mantém. Um formato que combina com várias variantes (digamos { x: number }, se duas o tivessem) mantém todas elas.
NonNullable
NonNullable<T> é a versão de tipo de uma verificação !== null && !== undefined. Ele aparece quando você deriva um tipo de algo opcional:
User["manager"] é { name: string } | null | undefined (o ? adiciona undefined), e NonNullable tira os dois. Desde o TypeScript 5.5, o compilador também infere esse predicado sozinho, então raw.filter((s) => s != null) já tem o tipo string[]; o s is NonNullable<typeof s> explícito deixa a intenção visível e funciona em versões mais antigas. A definição é type NonNullable<T> = T & {}: o tipo de objeto vazio {} aceita todo valor exceto null e undefined, então fazer a interseção com ele remove exatamente esses dois.
Como eles são montados
Exclude e Extract são conditional types de uma linha na biblioteca padrão:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
Quando T é uma union, um conditional type é aplicado a cada membro separadamente (ele se distribui), e membros never desaparecem do resultado. Então Exclude<"a" | "b", "a"> vira ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b"), que é never | "b", que é "b". A página de conditional types cobre a distribuição em detalhe.
Exclude vs Omit, Extract vs Pick
Os nomes são fáceis de confundir porque os dois pares "removem" ou "mantêm" alguma coisa. A diferença é sobre o que eles operam:
| Funciona com | Remove ou mantém | Exemplo | |
|---|---|---|---|
Exclude<T, U> | uma union | membros da union | Exclude<"a" | "b", "a"> → "b" |
Extract<T, U> | uma union | membros da union | Extract<"a" | "b", "a"> → "a" |
Omit<T, K> | um tipo de objeto | propriedades | Omit<User, "password"> |
Pick<T, K> | um tipo de objeto | propriedades | Pick<User, "name"> |
Usar Exclude em um tipo de objeto é um deslize comum, e ele falha em silêncio:
Account não é atribuível a "password", então Exclude o mantém inteiro. O próprio Omit é montado com Exclude aplicado às chaves: Omit<T, K> é Pick<T, Exclude<keyof T, K>>. Veja Omit para mais detalhes.
Um Exclude mais estrito
O segundo parâmetro de Exclude aceita qualquer tipo, então um erro de digitação não remove nada e não gera erro. Adicionar uma restrição faz o compilador verificar se o que você exclui está mesmo na union:
index.ts(7,37): error TS2344: Type '"drafft"' does not satisfy the constraint 'Status'.
Corrija a grafia e compila, com Strict igual a "published" | "archived". O mesmo truque funciona para Extract e para um Omit estrito (K extends keyof T).
Perguntas frequentes
O que Exclude faz no TypeScript?
Exclude<T, U> remove da union T todo membro que seja atribuível a U. Exclude<"a" | "b" | "c", "a"> é "b" | "c". Para remover vários membros, passe uma union como U: Exclude<T, "a" | "b">.
Qual a diferença entre Exclude e Extract?
São opostos. Exclude<T, U> mantém os membros de T que não são atribuíveis a U; Extract<T, U> mantém só os que são. Para o mesmo T e o mesmo U, os dois resultados juntos formam T.
Qual a diferença entre Exclude e Omit?
Exclude trabalha com um union type e remove membros da union. Omit trabalha com um tipo de objeto e remove propriedades. Exclude<User, "password"> não faz nada útil (retorna User), enquanto Omit<User, "password"> dá o tipo de objeto sem essa propriedade. Omit é construído sobre Exclude: ele exclui a chave de keyof T.
O que NonNullable faz no TypeScript?
NonNullable<T> remove null e undefined de um tipo: NonNullable<string | null | undefined> é string. Ele é definido como T & {}, já que {} aceita todo valor exceto null e undefined.
Por que Exclude não dá erro para um membro que não existe?
O segundo parâmetro dele não tem restrição, então qualquer tipo é aceito, e um erro de digitação como Exclude<Status, "drafft"> não remove nada em silêncio. Defina uma versão mais estrita, type StrictExclude<T, U extends T> = Exclude<T, U>, para que o compilador verifique se U faz parte de T.