Eu tenho uma biblioteca que exporta um tipo de utilitário semelhante ao seguinte:
type Action<Model extends object> = (data: State<Model>) => State<Model>;
Este tipo de utilitário permite declarar uma função que será executada como uma "ação". Ele recebe um argumento genérico contra o Model
qual a ação operará.
O data
argumento da "ação" é digitado com outro tipo de utilitário que eu exporto;
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
O State
tipo de utilitário basicamente pega o Model
genérico recebido e cria um novo tipo em que todas as propriedades do tipo Action
foram removidas.
Por exemplo, aqui está uma implementação básica do usuário acima;
interface MyModel {
counter: number;
increment: Action<Model>;
}
const myModel = {
counter: 0,
increment: (data) => {
data.counter; // Exists and typed as `number`
data.increment; // Does not exist, as stripped off by State utility
return data;
}
}
O acima está funcionando muito bem. 👍
No entanto, há um caso com o qual estou lutando, especificamente quando uma definição de modelo genérico é definida, juntamente com uma função de fábrica para produzir instâncias do modelo genérico.
Por exemplo;
interface MyModel<T> {
value: T; // 👈 a generic property
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Does not exist 😭
data.doSomething; // Does not exist 👍
return data;
}
};
}
No exemplo acima, espero que o data
argumento seja digitado onde a doSomething
ação foi removida e a value
propriedade genérica ainda exista. No entanto, esse não é o caso - a value
propriedade também foi removida por nosso State
utilitário.
Acredito que a causa disso é que T
é genérico, sem que sejam aplicadas restrições / restrições de tipo, e, portanto, o sistema de tipos decide que se cruza com um Action
tipo e subsequentemente o remove do data
tipo de argumento.
Existe uma maneira de contornar essa restrição? Eu fiz algumas pesquisas e esperava que houvesse algum mecanismo no qual eu pudesse afirmar que T
existe, exceto um Action
. ou seja, uma restrição de tipo negativo.
Imagine:
function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {
Mas esse recurso não existe para o TypeScript.
Alguém sabe como eu poderia fazer isso funcionar como eu esperava?
Para ajudar na depuração, aqui está um trecho de código completo:
// Returns the keys of an object that match the given type(s)
type KeysOfType<A extends object, B> = {
[K in keyof A]-?: A[K] extends B ? K : never
}[keyof A];
// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
// My utility function.
type Action<Model extends object> = (data: State<Model>) => State<Model>;
interface MyModel<T> {
value: T; // 👈 a generic property
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Does not exist 😭
data.doSomething; // Does not exist 👍
return data;
}
};
}
Você pode jogar com este exemplo de código aqui: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14