Pesquisa de site

Typescript: o sistema de tipos não consegue provar algo que parece obviamente verdadeiro


O seguinte gera um erro de compilação em getWrapper:

type Wrapper<K> = {
    value: K
}

type Wrappers = {
    [K in 'henk' | 'piet']: Wrapper<K>
}
const wrappers: Wrappers = {
    'henk': { value: 'henk' },
    'piet': { value: 'piet' }
}

function getWrapper<K extends keyof Wrappers>(k: K): Wrapper<K> {
    return wrappers[k]
}

Diz que wrappers[k] é Wrapper<'henk'> | Wrapper<'piet'>. Deveria ser capaz de perceber que wrappers[k] é na verdade Wrapper<K>. Posso ajudar o TypeScript a perceber isso?

Resposta #1

ATUALIZAÇÃO: 30-05-2019 o lançamento do TypeScript 3.5 introduz uma verificação de tipo união mais inteligente, mas não corrige este problema, provavelmente porque está usando um tipo genérico K em vez de uma união de tipos concretos conhecidos. Então, não há alterações na solução abaixo até o momento.

Tenho quase certeza de que o seu problema é que o TypeScript não reduz uniões da maneira que você espera. Por exemplo, você pode esperar que o seguinte funcione:

declare const innerUnion: { foo: string | number };
const outerUnion: { foo: string } | { foo: number } = iU; // error

Mas não é assim. O TypeScript não reduz de forma agressiva o tipo de outerUnion para o tipo de innerUnion. A razão pela qual o compilador não faz isso automaticamente parece ser o pragmatismo: em muitos casos, tal redução está errada porque há outra propriedade que não pode ser mesclada:

declare const iU: { foo: string | number, bar: string | number };
const oU: { foo: string, bar: string } | { foo: number, bar: string } = iU; // error

O erro acima é bom, já que talvez iU seja { foo: 0, bar: '' }.

O TypeScript pensa que K pode ser do tipo 'henk' | 'piet', e portanto a saída da função poderia ser Wrapper<'henk' | 'piet'>, mas o valor retornado é do tipo Wrapper<'henk'> | Wrapper<'piet'>. E como a redução da união acima é algo que o TypeScript não faz, ele diz que há um erro.

Então, por que o valor retornado tem o tipo Wrapper<'henk'> | Wrapper<'piet'>? Porque indexar objetos com uma união de chaves realmente resulta na união dos valores das propriedades desse objeto.

Isso sugere uma solução para o seu problema, use um tipo de pesquisa para representar essa operação de indexação:

function getWrapper<K extends keyof Wrappers>(k: K): Wrappers[K] {
    return wrappers[k]; // okay
}

Isso faz o que você quer, eu acho. Espero que ajude; boa sorte!