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
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.