Home > OS >  How to create a type that extract all methods from a class in typescript?
How to create a type that extract all methods from a class in typescript?

Time:12-21

Let's say I have a class Foo

class Foo {
  bar(){
     // anything
  }

  baz() {
    // anything
  }
}

How can I create a type ExtractMethods that will receive a class and return an interface or a type with the class methods?

e.g.

type FooMethod = ExtractMethods<Foo> // => { bar(): void; baz(): void; } 

I know one common comment to this questions will be "Why don't you just create a interface for Foo and use that interface you created?"

But in the case I want to solve, Foo isn't a class I created, but a third party class. I could create an interface for Foo, but it would never reflect perfectly the methods, in case the class changes in the future with updates I would need to also update the interface and this is something I'm trying to avoid.

CodePudding user response:

Based on this article, I came up with the following solution (playground):

class Test {
  foo() {}
  bar() {}
  zar = 3;
}

type SubType<Base, Condition> = Pick<Base, {
    [Key in keyof Base]: Base[Key] extends Condition ? Key : never
}[keyof Base]>;

type MethodsOnly<T> = SubType<T, () => unknown>;

type T = Pick<Test, keyof MethodsOnly<Test>>

const test = {} as T;

test.bar; // legal
test.foo; // legal
test.zar; // Property 'zar' does not exist on type 'T'.(2339)

CodePudding user response:

Let's write a type function PickMatching<T, V> which takes an object type T and a property value type V and evaluates to another object type only those properties of T whose property types are assignable to V. This is straightforward if we use key remapping in mapped types:

type PickMatching<T, V> =
    { [K in keyof T as T[K] extends V ? K : never]: T[K] }

By mapping a key to never we are essentially omitting the property from the mapped type. Then we can express ExtractMethods<T> by picking all function-valued properties from T:

type ExtractMethods<T> = PickMatching<T, Function>;

This produces the following results:

class Foo {
    bar() { }
    baz() { }
    notAMethod = 123;
    funcProp = () => 10;
}

type FooMethod = ExtractMethods<Foo>
/* type FooMethod = {
    bar: () => void;
    baz: () => void;
    funcProp: () => number;
} */

As you see, the number-typed notAMethod is not present in FooMethod. The methods bar() and baz() appear in FooMethod as desired. Importantly, the function-typed funcProp is also present in FooMethod. The type system cannot reliably distinguish a method from a function-valued property. The main difference is whether or not the member exists on the instance directly or on the class prototype, but the type system treats class instances and class prototypes as the same type.

That's really the best you can do to implement ExtractMethods generally.


On the other hand, if you want to do it just for a particular class like Foo and can drop down from the type level to the value level, you can use the fact that the compiler understands that spreading an instance of a class into a new object will only give you the instance properties:

const spreadFoo = { ... new Foo() };
/* const spreadFoo: {
  notAMethod: number;
  funcProp: () => number;
} */

and from that you can compute FooMethod (assuming that you don't have any non-method prototype members):

type FooMethod = Omit<Foo, keyof typeof spreadFoo>;
/* type FooMethod = {
  bar: () => void;
  baz: () => void;
} */

But it depends on your use cases whether or not this sort of approach is feasible.

Playground link to code

  • Related