2

Let's say I have this simple class in TS.

class MyClass {
  foo() {
    console.log('foo');
  }

  bar(n: number) {
    console.log(n);
  }
}

const instance = new MyClass();
instance.foo(); // prints 'foo'

I would like to add an instance method delay with this signarture:

delay(ms = 1000): Partial<MyClass>;

It should be called from the class instance, and be able to chain into other class methods, but fire them after a timeout, i.e.:

instance.foo(); // prints 'foo' immediately
instance.delay().foo(); //prints 'foo' after 1 second
instance.delay(2000).foo(); // prints 'foo' after 2 seconds

I tried constructing this method myself in what looked like a way that made sense, but I'm facing TS errors:

delay(ms = 1000): Partial<MyClass> {
  const methods = [this.foo, this.bar];
  return methods.reduce<Partial<MyClass>>((acc, method) => {
    acc[method.name] = async (...args: Parameters<typeof method>) => {
      await new Promise(resolve => { setTimeout(resolve, ms); });
      return method.apply(this, args);
    };
    return acc;
  }, {});
}

I'm pulling the methods from the class that I would like to be available for delay, then reducing them into a partial object and creating a new method per method, the awaits a timeout internally. But I'm getting some TS errors:

acc[method.name]
/* Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Partial<MyClass>'.
  No index signature with a parameter of type 'string' was found on type 'Partial<MyClass>'. */

method.apply(this, args);
/* Source provides no match for required element at position 1 in target. /*

The second one is because different methods might have different params, and the first one is unclear to me.

Ronny Efronny
  • 1,148
  • 9
  • 28
  • Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – mousetail Aug 14 '23 at 06:41
  • @mousetail This isn't my issue, I know how to return from an asynchronous call. My problem is actually creating this function that wraps other instance methods in a timeout, in a TS-compliant way. – Ronny Efronny Aug 14 '23 at 06:48

1 Answers1

0

For acc[method.name] Typescript complains because the method names of the class are string literals and so a string, which is a wider type, cannot be used to index the type acc, so we need to add an as assertion for it like so -

acc[method.name as keyof typeof acc]

Once we do that, Typescript complains with -

Type '(n: number) => Promise' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.

Which happens because of this line -

const methods = [this.foo, this.bar];

Type type of this.foo is () => void and when typescript narrows types, specially for functions, an empty parameter matches all types of parameters, and the return type should be same for one to extend the other, in this case, both foo and bar return void so type, and foo takes no parameters, so the type is narrowed to ((n: number) => void)[] which is why we'd need to provide it an as const assertion to make Typescript consider that both types are required as is like so -

const methods = [this.foo, this.bar] as const;

After that, Typescript complains with -

The 'this' context of type '(() => void) | ((n: number) => void)' is not assignable to method's 'this' of type '(this: this) => void'. Type '(n: number) => void' is not assignable to type '(this: this) => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.

Since we added an as const assertion for methods, the type of args become [] | [number], what Typescript is complaining about now is that the context for this, which is our original instance, cannot be applied to the method because, for this the method types are narrowed as explained above, and the type of method being asserted with as const isn't, so we need to add an as assertion for the arg parameter here like so -

return method.apply(this, args as [number]);

Now, all errors are gone.

So this is what we end up with -

class MyClass {
    foo() {
        console.log('foo');
    }

    bar(n: number) {
        console.log(n);
    }
    delay(ms = 1000): Omit<MyClass, "delay"> {
        const methods = [this.foo, this.bar] as const;
        return methods.reduce<Omit<MyClass, "delay">>((acc, method) => {
            acc[method.name as keyof typeof acc] = async (...args: Parameters<typeof method>) => {
                await new Promise(resolve => { setTimeout(resolve, ms); });
                return method.apply(this, args as [number]);
            };
            return acc;
        }, {} as Omit<MyClass, "delay">);
    }
}

Here's a Playground link.

NOTE - The return type is changed to Omit<MyClass, "delay"> from Partial<MyClass> to avoid errors of the method being possibly undefined.

0xts
  • 2,411
  • 8
  • 16
  • Thank for the answer. In my example I was not changing the instace, I was trying to create an object like you dod, but using a reducer over methods that I wanted. Any way to utilize that approach instead of manually defining each new method? – Ronny Efronny Aug 14 '23 at 07:31
  • Yes, I'll update my answer. – 0xts Aug 14 '23 at 07:35