typeguards - Como mapear tipos e adicionar type guards baseados em this no TypeScript?
Suponha que exista uma interface com membros arbitrários assim:
interface A extends Record<string, any[]>{
// Later used by interface merging
}
//maybe
declare module "./main"{
interface A{
func: [arg1:string]
}
}
E eu quero mapear A assim, com esses guards de tipo baseados em isto:
interface Try1<T extends keyof A>{
[K in keyof A]: (...args: A[K]) => this is Try1<K>
// Lots of errors
}
type Try2<T extends keyof A> = {
[K in keyof A]: (...args: A[K]) => this is Try1<K>
// Error: A 'this' type is available only in a non-static member of a class or interface.ts(2526)
}
Então, como eu posso fazer isso?
Uso real:
interface Elements extends Record<string, [any[], any]>{
// Later used by interface merging
}
interface Elements{
button: [[text: string], MouseEvent];
}
class ImguiContext<CurrentEv extends keyof Elements>{
// !!!
[E in keyof Elements]: (...args: Elements[E][0]) => this is ImguiContext<E>;
event: Elements[CurrentEv][1];
}
function main(_: ImguiContext){
if(_.button("Click me")){
// now `_.event` should be `MouseEvent`
console.log("clicked at X:", _.event.screenX);
}
}
Outra possível solução, mas falhou
let a:string|number = 1;
function aIsNumber(/*no parameter*/)/*: ???*/ {
return typeof a === "number";
}
??? pode ser
- Funções de
asserçãocomoasserts condition Predicados de tipocomoa é número
mas ambos falharam
Resposta #1
Aparentemente, é uma funcionalidade ausente do TypeScript permitir predicados de tipo "this" (como originalmente implementado em microsoft/TypeScript#5906) em tipos mapeados. Se você tentar fazer isso, receberá o erro que mencionou. Há uma solicitação de funcionalidade aberta em microsoft/TypeScript#51958 para dar suporte a isso, mas, por enquanto, não faz parte oficialmente da linguagem. O problema está listado como "Aguardando mais feedback", então, se você quiser que isso aconteça, não faria mal ir lá, dar um 👍 e descrever por que seu caso de uso é relevante.
Curiosamente, nessa edição é observado que—apesar do erro—o código na verdade se comporta como esperado.
Então, alguém poderia possivelmente emitir uma diretiva de comentário //@ts-ignore para suprimir o erro e tratar isso como um "recurso não suportado". Aqui está como isso poderia parecer para o seu exemplo:
interface Elements {
button: [[text: string], MouseEvent];
other: [[age: number, isSquishy: boolean], Date];
}
type ImguiContext<K extends keyof Elements = keyof Elements> = {
//@ts-ignore this is usually a bad idea
[E in keyof Elements]: (...args: Elements[E][0]) => this is ImguiContext<E>;
} & { event: Elements[K][1]; }
function main(_: ImguiContext) {
if (_.button("Click me")) {
// now `_.event` should be `MouseEvent`
console.log("clicked at X:", _.event.screenX); // okay
} else if (_.other(2, false)) {
console.log("otherThing", _.event.getFullYear()); // okay
}
}
Eu realmente não recomendaria fazer isso, no entanto. A diretiva //@ts-ignore apenas impede que o erro seja exibido; ela não corrige o problema subjacente que o compilador tem. Talvez isso esteja tudo bem porque o problema subjacente está limitado ao erro reportado, ou talvez você encontre problemas indiretos estranhos em outras partes do seu código por causa disso. A documentação da diretiva diz que ela deve ser usada "com muita parcimônia" (ênfase deles). Então use por sua própria conta e risco.
Em vez disso, eu estaria inclinado a envolver seu contexto em uma versão cujo tipo o TypeScript possa representar sem erros. Se, em vez de ser um tipo mapeado, ImguiContext tivesse um método test() que aceitasse o nome e os argumentos, você poderia reescrevê-lo como uma interface simples:
interface ImprovedContext<T = unknown> {
test<E extends keyof Elems>(
name: E, ...args: Elems[E][0]
): this is ImprovedContext<Elems[E][1]> // no ts-ignore needed
event: T
}
Se você precisar escrever uma função auxiliar para envolver seu contexto antigo no novo, você pode, já que isso apenas requer uma asserção de tipo, que é menos perigosa do que //@ts-ignore:
function improveContext(_: ImguiContext): ImprovedContext {
return {
test(n, ...args) { return (_[n] as any)(...args) },
event: _.event
}
}
E então as coisas ainda funcionam:
function main2(_Orig: ImguiContext) {
const _ = improveContext(_Orig);
if (_.test("button", "Click me")) {
console.log("clicked at X:", _.event.screenX); // okay
} else if (_.test("other", 2, false)) {
console.log("otherThing", _.event.getFullYear()); // okay
}
}
Claro que depende do seu caso de uso se é mais viável depender de um recurso não suportado ou refatorar para uma versão que seja suportável.
Link do playground para o código
