Digamos que eu tenha algo assim:
type TIn = {
xa?: number;
xb?: string;
xc?: boolean;
// ...
}
type TOut = {
ya: number | undefined;
yb: string | undefined;
yc: TPerson | undefined;
// ...
}
type TPerson = {
name: string;
age: number;
}
function fn(input: TIn): TOut {
// ...
}
Agora, digamos que eu queira aplicar a verificação estática de nulidade dos campos de saída em relação à entrada. Por exemplo:
quando
xa
é umnumber
, o relacionadoya
também será umnumber
;quando
xa
éundefined
,ya
será tambémundefined
;quando
xb
é umstring
, o relacionadoyb
pode ser umstring
ouundefined
;quando
xb
éundefined
,yb
será tambémundefined
;
E assim por diante.
Em C#, há atributos especiais para instruir o compilador sobre como verificar estaticamente a nulidade.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-análise
Gostaria de saber se existe algum "substituto" (quero dizer, brincar com tipos e utilitários) no TypeScript para obter uma verificação de nulidade mais completa.
Além disso, não é uma solução personalizada "como este exemplo", mas sim uma ferramenta modular para usar em qualquer caso.
EDIT: Aqui está uma tentativa de resolver, não está funcionando, mas foi adicionada para maior clareza.
Abaixo estão os supostos ajudantes:
//maps TResult when T1 is not undefined
//undefined otheriwse
type TNotNull<T1, TResult> = T1 extends {}
? TResult
: undefined;
//maps TResult | undefined when T1 is not undefined
//undefined otheriwse
type TMaybeNull<T1, TResult> = T1 extends {}
? TResult | undefined
: undefined;
//maps TResult when both T1 and T2 are not undefined
//undefined otheriwse
type TNotNullWhenBoth<T1, T2, TResult> = T1 extends {}
? T2 extends {} ? TResult : undefined
: undefined;
Poderia haver muito mais (mais argumentos, lógica mais complexa, etc.)
Agora, como exemplo, vamos definir o contrato de entrada da função:
type TIn = {
a?: number;
b?: number;
s?: string;
}
Para o tipo de saída, a ideia é compô-lo usando os supostos auxiliares:
type TOut<T extends TIn> = {
inv_a: TNotNull<T["a"], number>;
flag: TMaybeNull<T["b"], boolean>;
str: TNotNull<T["s"], string>;
sum: TNotNullWhenBoth<T["a"], T["b"], number>;
}
Ou seja, para inv_a
:
- quando
a
é um número,inv_a
também é um número - quando
a
é indefinido,inv_a
também é indefinido
Para flag
:
- quando
b
é um número,flag
é um booleano ou indefinido - quando
b
é indefinido,flag
também é indefinido
Para str
, similarmente a inv_a
:
- quando
s
é uma string,str
também é uma string - quando
s
em indefinido,str
também é indefinido
Para sum
, mesma lógica que inv_a
, mas dois argumentos AND-ed:
- quando ambos
a
eb
são números,sum
também é um número sum
será indefinido caso contrário
Por fim, vamos escrever a função. Aqui, a implementação não é importante. Fora da função, precisamos apenas que o contrato de entrada/saída seja sempre satisfeito.
const flags: Array<boolean> = new Array(10);
flags[3] = true;
function fn(input: TIn): TOut<TIn> {
const { a, b, s } = input;
//just an example of implementation
const inv_a = typeof a === "number"
? 1 / a
: void 0;
const flag = typeof b === "number"
? flags[b]
: void 0;
const str = typeof s === "string"
? s + "hello!"
: void 0;
const sum = typeof a === "number" && typeof b === "number"
? a + b
: void 0;
return { inv_a, flag, str, sum }
}
Neste ponto, aqui estão alguns casos de uso e como a inferência de resultados é esperada:
//expected inference: all undefined
//actual inference: inv_a:number|undefined; flag:boolean|undefined; etc
const { inv_a, flag, str, sum } = fn({});
//expected inference: flag:boolean | undefined; rest undefined
//actual inference: inv_a:number|undefined; flag:boolean|undefined; etc
const { inv_a, flag, str, sum } = fn({ b: 3 });
//expected inference: inv_a:number; str:string; flag, sum:undefined
//actual inference: inv_a:number|undefined; flag:boolean|undefined; etc
const { inv_a, flag, str, sum } = fn({ a: 5, s: "xyz" });
//expected inference: inv_a, sum:number; flag, str:undefined
//actual inference: inv_a:number|undefined; flag:boolean|undefined; etc
const { inv_a, flag, str, sum } = fn({ a: 5, b: 3 });
Infelizmente, não funciona.
Você precisa declarar vários tipos que deseja que sejam aceitáveis, por exemplo
E então combine-os em uma união:
TProps = TIn1 | TIn2 | TIn3
Há apenas algumas correções para tornar o novo código de exemplo da pergunta funcional.
Testado no playground TypeScript .
Resposta antes da pergunta Editar:
Tipos mapeados https://www.typescriptlang.org/docs/handbook/2/mapped-types.html parece ser o que você está procurando.
Consegui fazer isso:
As interfaces definem: estrutura do objeto de entrada, remapeamento das chaves de propriedade de saída para as de entrada, tipos relacionados aos campos do objeto de entrada, se presentes.
TIn
a interface é remapeada para definir o modelo de entradaT
e torna as propriedades do objeto de entrada opcionais.Fiz alguns testes:
Obrigado ao jcalz (veja os comentários da pergunta), que me sugeriu o caminho correto.
Minha tentativa estava correta, mas falhou em um ponto fundamental: instruir a dependência de entrada/saída declarando a função como genérica.
Então isso vai funcionar: