Reputation: 43
Let's say I wanted to implement a typed function chain in TypeScript, but in this case, calling a function removes that function from the return type. For example:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
interface Chainable {
execute: () => Promise<void>;
}
interface Chain1 extends Chainable {
chain1?: () => Omit<this, 'chain1'>;
}
interface Chain2 extends Chainable {
chain2?: () => Omit<this, 'chain2'>;
}
let chain: Chain1 & Chain2 = {
execute: () => null,
chain1: () => {
delete chain.chain1;
return chain;
},
chain2: () => {
delete chain.chain2;
return chain;
}
};
chain.chain1().chain2().execute(); // Using the function chain
When I call chain.chain1()
, I would get Pick<Chain1 & Chain2, "execute" | "chain2"
as the return type, which is great, since it prevents me from calling chain1
twice.
However, as soon as I chain it with the chain2
function, the return type becomes Pick<Chain1 & Chain2, "chain1" | "execute"
. This would allow me to call chain1
again, which is what I'm trying to prevent. Ideally, the compiler would complain that Property 'chain1' does not exist on type
:
chain.chain1().chain2().chain1(); // I want this to return a compiler error :(
Am I going about this the right way? Is it possible in TypeScript to progressively combine multiple Omit types together, so that the return types continuously omit properties?
Upvotes: 4
Views: 1355
Reputation: 249536
I think the type for this
is determined when the function is first checked and then not reevaluated at any point after. So this
for the second call of chain2
will still be the original this
not the return type of chain1
. I am not sure if this is the intended behavior or a bug, you might want to check GitHub for similar issues.
One workaround is to capture this
for any given function using a generic type parameter which will be tied to this
. This will ensure correct type flow through the function chain. One small issue is that the typings will not work out using arrow function, you will need to use regular functions and access this
:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
interface Chainable {
execute: () => Promise<void>;
}
interface Chain1 extends Chainable {
chain1?: <T extends Chain1>(this: T) => Omit<T, 'chain1'>;
}
interface Chain2 extends Chainable {
chain2?: <T extends Chain2>(this: T) => Omit<T, 'chain2'>;
}
let chain: Chain1 & Chain2 = {
execute: () => null,
chain1: function () {
delete this.chain1;
return this;
},
chain2: function () {
delete this.chain2;
return this;
}
};
chain.chain1().chain2().execute(); // Using the function chain
chain.chain1().chain2().chain1().execute(); // error
Upvotes: 5