Pesquisa de site

O guardião de tipo em propriedade de objeto genérico não funciona no TypeScript


Como exemplo, eu tenho uma função muito simples que define "true" (booleano) para alguma propriedade de objeto nomeada:

function setTrue<T, K extends keyof T>(host: T, property: K) {
    if (typeof host[property] === 'boolean')
        host[property] = true; // <-- TS2322: Type 'true' is not assignable to type 'T[K]'.
}

Não consegue descobrir o que está errado? Parece que o guardião de tipo typeof host[property] === 'boolean' não funciona neste caso. Quem sabe como alcançar a verificação de tipo estático correta neste exemplo?

A única solução alternativa que encontrei é se T extends any, mas nesse caso nenhum guarda de tipo é necessário:

function setTrue<T extends any, K extends keyof T>(host: T, property: K) {
    host[property] = true; // <-- Always Ok
}

Resposta #1

Acho que há várias coisas contribuindo para esta situação.

Um problema é que o compilador não realiza a redução do tipo de guarda quando a chave da propriedade que está sendo testada é uma variável; veja microsoft/TypeScript#10530 para mais informações.

Outro fator é que uma leitura e subsequente escrita de uma propriedade cujo tipo de chave não é conhecido como singleton não pode ser feita de forma segura. Veja microsoft/TypeScript#32693. O compilador não percebe que property é exatamente a mesma chave nas duas linhas. Tudo o que ele entende é que as propriedades acessadas possuem o mesmo tipo, o que não é suficientemente rigoroso, a menos que os tipos já sejam conhecidos como singleton.

Finalmente, o compilador simplesmente não é muito bom em análise de fluxo de controle diante de valores que dependem de parâmetros de tipo genérico não especificados como T e K. Mesmo se você testar property para ser um subtipo específico de keyof T, o compilador não irá restringir o parâmetro K, veja microsoft/TypeScript#24085 ou microsoft/TypeScript#33014. Na melhor das hipóteses, ele irá restringir o tipo de property para alguma interseção K & ..., que pode ou não ser útil.


Uma ressalva a apresentar aqui antes de mostrar soluções alternativas: isso não é tecnicamente seguro. Se o seu host tiver uma propriedade cujo tipo seja conhecido por ser o literal booleano false, como em:

interface Foo {
  bar: false
}
declare const foo: Foo;

Então você vai encontrar problemas:

setTrue(foo, "bar");
foo.bar; // false or true?

Sua implementação de setTrue() apenas verifica se foo.bar é do tipo boolean, o que ele é. Mas agora mentimos para o compilador ao definir uma propriedade de tipo false para o valor true. Então foo.bar agora é true em tempo de execução, mas false no seu IDE.

Vou ignorar essa ruga, mas ela ainda está lá, então tenha cuidado.


Então, soluções alternativas. A mais fácil de longe é simplesmente usar uma afirmação de tipo e seguir em frente:

function setTrue<T, K extends keyof T>(host: T, property: K) {
  if (typeof host[property] === 'boolean')
    (host as any as Record<K, boolean>)[property] = true; 
}

Aqui afirmamos que se host[property] for do tipo boolean, então trataremos host como Record<K, boolean>: um objeto com valores de propriedades do tipo boolean em chaves do tipo K.

Menos direto é refatorar em algumas partes menores que o compilador possa verificar sozinho ou que possam ser transformadas em um guardião de tipo definido pelo usuário, para conduzir o compilador pela análise de tipos da maneira que queremos:

function setTrueKnownBoolean<K extends PropertyKey>(host: Record<K, boolean>, property: K) {
  host[property] = true;
}

function isBooleanProperty<K extends keyof T, T>(
  host: T, property: K): host is T & Record<K, boolean> {
  return typeof host[property] === "boolean";
}

function setTrue2<K extends keyof T, T>(host: T, property: K) {
  if (isBooleanProperty(host, property)) {
    setTrueKnownBoolean(host, property);
  }
}

A função setTrueKnownBoolean só pode ser chamada em objetos host que são conhecidos por ter propriedades boolean na(s) chave(s) property, portanto a implementação não precisa de verificação de tipo internamente. A função isBooleanProperty atua como um guardião de tipo para restringir o tipo de host de T para T & Record<K, boolean>. E agora setTrue2() pode ser implementada compondo essas duas outras funções.


Link do playground para o código