Sasha
Sasha

Reputation: 43

Function chaining in TypeScript with Omit types

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

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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

Related Questions