Home > Enterprise >  TypeScript: Create type that removes properties of an interface/class based on their value-types
TypeScript: Create type that removes properties of an interface/class based on their value-types

Time:11-07

Imagine the class:

class Person {
  name = "Ann";
  sleep() { /*...*/ }
  eat() { /*...*/ }
}

Now I would like to create a type that extracts only the methods:

// should work:
const personMethods: MethodsOf<Person> = { sleep() {}, eat() {} };
const names: keyof MethodsOf<Person>[] = ['sleep', 'eat'];
// should fail:
const personMethods: MethodsOf<Person> = { notAPersonMethod() {} };
const names: keyof MethodsOf<Person>[] = ['not', 'existing', 'methods'];

I could not find anything in the typescript docs, and my attempts failed so far. I tried:

type MethodKeys<
  Type extends object,
  Key extends keyof Type,
> = keyof Record<Type[Key] extends AnyFn ? Key : never, any>;

type Methods<Type extends object> = {
  [Property in MethodKeys<Type, keyof Type>]: Type[Property];
};

const test: MethodKeys<Person> = 'name'; // works, but shouldn't :(
const test2: Partial<Methods<Person>> = { name: 'Ann' }, // works too :/

Thanks for your help!

CodePudding user response:

In addition to @Chase's solution, you could use distributive conditional types in order to filter out the keys that have a function value:

type MethodHelper<T, K extends keyof T> = K extends any
    ? T[K] extends Function ? K
    : never : never;

Then use that to implement MethodKeys and Methods:

type MethodKeys<T> = MethodHelper<T, keyof T>;
type Methods<T> = {
    [K in MethodKeys<T>]: T[K]
};


const test: MethodKeys<Person> = 'name'; // doesn't work
const test2: MethodKeys<Person> = 'sleep'; // does work
const test3: Partial<Methods<Person>> = { name: 'Ann' }; // doesn't work
const test4: Partial<Methods<Person>> = { sleep() {} }; // does work

Playground link

CodePudding user response:

All you need is a type that gives you back the method names given the class/interface. You can use a mapped type to create an object type where each value represents each method names. Then, you can index that object type to get its value type - which will yield a union of all method names.

type MethodKeys<T> = {
    [K in keyof T]: T[K] extends (...x: any) => any ? K : never;
}[keyof T]

Now, you can use Pick to pick those method keys from your class/interface.

type MethodsOf<T> = Pick<T, MethodKeys<T>>;

And your given testcases work correctly-

// works
const personMethods: MethodsOf<Person> = { sleep() {}, eat() {} };
const names: Array<keyof MethodsOf<Person>> = ['sleep', 'eat'];

const personMethods1: MethodsOf<Person> = { notAPersonMethod() {} };
// ^ Type '{ notAPersonMethod(): void; }' is not assignable to type 'MethodsOf<Person>'.
const names1: Array<keyof MethodsOf<Person>> = ['not', 'existing', 'methods'];
// ^ Type '"not"' is not assignable to type 'MethodKeys<Person>'.
// ...and more

Check it out on playground.

  • Related