Uma restrição de generic limita o que um type parameter pode ser. <T extends { length: number }> aceita só tipos que têm um length numérico (strings, arrays, seus próprios objetos) e, por causa dessa promessa, a função pode ler .length.
O tipo de retorno continua sendo T, e não { length: number }: passar dois arrays de números devolve number[], e os objetos mantêm o tag. A restrição descreve o mínimo; o tipo de quem chama é preservado.
Por que as restrições são necessárias
Dentro de uma função genérica, um T sem restrição poderia ser qualquer coisa: uma string, um número, null, uma função. Então o TypeScript só permite o que funciona em todos os tipos, que é quase nada.
O compilador mostra:
index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.
A solução é dizer do que você precisa: <T extends { length: number }>. As restrições transformam "qualquer tipo" em "qualquer tipo que tenha isto", que normalmente é o que uma função genérica quer dizer.
Restringindo a uma interface
A restrição pode ser qualquer tipo, incluindo uma interface ou type alias que você já tem. A função então funciona com qualquer coisa que tenha pelo menos aqueles membros, e continua retornando o tipo completo de quem chama.
Compare com um tipo de parâmetro simples: function byId(items: HasId[], id: number): HasId | undefined aceita os mesmos arrays, mas o resultado é só HasId, então found.name seria um erro. A versão genérica repassa o tipo completo.
K extends keyof T: acesso seguro a propriedades
A restrição mais comum relaciona dois type parameters. K extends keyof T diz que K precisa ser um dos nomes de propriedade de T, e T[K] é o tipo dessa propriedade.
Uma chave digitada errado ou inexistente é erro de compilação, e o tipo de retorno acompanha a chave: "year" dá number, "title" dá string. A página sobre keyof trata do próprio keyof, incluindo como ele se comporta com index signatures.
Restringindo a primitivos
Uma restrição pode ser um primitivo ou uma union deles. Um efeito colateral útil: quando T extends string (ou number), o TypeScript infere o literal type do argumento em vez de alargá-lo.
Sem a restrição, createEvent("user.saved") retorna { type: string }: aqui o TypeScript alarga para string o literal que inferiu para T. Com T extends string o literal é mantido, e é assim que helpers tipados de eventos e rotas obtêm nomes exatos a partir de argumentos string simples.
Erros comuns
Retornar algo que só corresponde à restrição. Uma função que retorna T precisa retornar um T, e não qualquer valor que caiba na restrição:
interface HasId {
id: number;
}
function reset<T extends HasId>(item: T): T {
return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
// '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
// instantiated with a different subtype of constraint 'HasId'.
Se T for { id: number; name: string }, o novo objeto não tem name, então não é um T. Retorne um spread da entrada, return { ...item, id: 0 };, que mantém todas as propriedades, ou declare o tipo de retorno como HasId se isso é tudo o que você produz.
Passar um argumento de tipo que quebra a restrição. Argumentos de tipo explícitos também são verificados: com function scale<T extends number>(x: T), a chamada scale<string>("2") falha com Type 'string' does not satisfy the constraint 'number'. (TS2344).
Restringir quando você não precisa de um generic. Se T aparece só no parâmetro, function print<T extends HasId>(item: T): void é só function print(item: HasId): void com sintaxe extra. As restrições importam quando T também aparece no tipo de retorno ou em outro parâmetro.
Restrições com valores padrão
Um type parameter pode ter uma restrição e um padrão ao mesmo tempo. O padrão precisa satisfazer a restrição.
interface Store<TState extends object = Record<string, unknown>> {
get(): TState;
set(next: Partial<TState>): void;
}
type AnyStore = Store; // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>; // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.
A mesma palavra-chave extends também aparece em conditional types, T extends string ? A : B, em que ela é um teste e não uma restrição; a página sobre conditional types trata dessa forma.
Perguntas frequentes
O que T extends significa em um generic do TypeScript?
<T extends Constraint> significa que T pode ser qualquer tipo atribuível a Constraint. Quem chama só pode passar tipos assim, e dentro da função você pode usar tudo o que a restrição garante. <T extends { length: number }> aceita strings, arrays e qualquer objeto com um length numérico, e deixa você ler value.length.
O que K extends keyof T significa?
K precisa ser um dos nomes de propriedade de T. Junto com o indexed access type T[K], isso tipa o acesso a propriedades com exatidão: function get<T, K extends keyof T>(obj: T, key: K): T[K] só aceita chaves reais de obj, e o tipo de retorno é o tipo daquela propriedade.
Por que recebo "Property does not exist on type T"?
Um T sem restrição poderia ser qualquer tipo, inclusive number ou null, então o TypeScript não permite nada específico de um tipo nele (TS2339). Adicione uma restrição que prometa a propriedade: <T extends { name: string }>, ou restrinja a uma interface que você já tem, <T extends User>.
O que significa "T could be instantiated with a different subtype of constraint"?
Erro TS2322: a função promete retornar T, mas retorna um valor que só corresponde à restrição. Se T for { id: number; name: string }, um novo { id: 1 } não é um T. Retorne o valor que você recebeu (ou um spread dele, tipado de acordo), ou mude o tipo de retorno para a restrição.
Qual é a diferença entre extends em um generic e extends em uma classe ou interface?
A mesma palavra-chave, ideias relacionadas. Em class Dog extends Animal e interface B extends A, ela declara herança. Em <T extends A>, declara uma restrição: T precisa poder ser atribuído a A. Em um conditional type, T extends A ? X : Y, é um teste que escolhe um de dois tipos.