typescript - Inferir valores de interface nunca
Encontrei um comportamento estranho. Versão do TypeScript: 4.9.4 Playground
interface TestInterface {
A: string
}
type Test = TestInterface extends Record<infer K, any> ? K : never;
let t1: Test; // A
type Test2 = TestInterface extends Record<any, infer V> ? 1 : never;
let t2: Test2; // never
type Test3 = TestInterface extends Record<infer K, infer V> ? V : never;
let t3: Test3; // string
Eu tenho a interface TestInterface.
- Quando eu tento inferir apenas chaves, funciona.
- Quando tento inferir valores, nunca.
- Quando tento inferir chaves e valores, funciona..
Por quê?
Resposta #1
Record tem 2 comportamentos e costuma confundir as pessoas o tempo todo, mas interfaces e assinaturas de índice também confundem as pessoas.
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
Quando K é uma união de subtipos de string, number ou symbol, como 'A' | 'B', isso resulta em um tipo literal de objeto, como o seguinte:
{ A: string, B: string }
Um tipo literal de objeto diz:
Eu sei sobre os campos listados aqui, mas pode haver qualquer número de campos supernumerários e eles podem ser de qualquer tipo
Quando K é uma união de exatamente string, number ou symbol, como string | number, isso resulta em uma assinatura de tipo, como a seguinte
{
[key: string]: string
[key: number]: string
}
Uma assinatura de tipo basicamente diz:
todo campo possível que corresponda a esse tipo de chave será desse mesmo tipo
E isso é muito diferente.
Record também é um pouco estranho, pois Record<any, unknown> avalia para {[k: string]: unknown} ao contrário de Record<PropertyKey, unknown> ou { [P in any]: unknown }, mas isso não é o que causou sua surpresa.
A conclusão é: quando você extend um tipo de literal de objeto ou uma assinatura de índice, não é de forma alguma o mesmo tipo de verificação.
A segunda coisa confusa é: interfaces não são finitas, devido à fusão de declarações.
interface TestInterface {
A: string
}
interface TestInterface {
B: number
}
// same as
interface TestInterface {
A: string
B: number
}
Se você pode mesclá-lo à vontade e até usar a ampliação de módulos, é difícil dizer que uma interface estende uma assinatura de índice a qualquer momento — pelo menos esse é o meu modelo mental e ele funciona muito bem.
A única exceção é se você restringir sua interface a uma assinatura de índice específica explicitamente:
interface TestInterface {
[k: string]: string
A: "foo"
}
interface TestInterface {
B: number
// ~~~~~~~~~ nope!
}
Então você está em posição de fazer promessas: se você extends Record<any, infer V>, V será exatamente string (mesmo que A seja "foo")
Agora, como mencionado nos comentários, quando você escreve keyof TestInterface você obtém as chaves da interface. Usá-las em Record retornará um tipo de objeto literal, e você estará bem.
interface TestInterface {
A: string
}
type S = TestInterface extends Record<keyof TestInterface, infer V> ? V : never; // string
Apenas observe que isso é verdadeiro apenas se a interface não tiver uma assinatura de índice explícita: se tiver, ela não retornará chaves conhecidas, como "A", mas algo como string | number (você sempre obtém number quando a chave é string), o que retornará uma assinatura de índice quando for usado no Record.
