millimoose
millimoose

Reputation: 39960

More readable way to extract keys of a type, where the types of the values for said key conform to an interface?

My app has a bunch of services at the boundary between the front-end and back-end that are called over Electron IPC using string names. To aid typechecking and editor support, I have an interface which maps service names to the interface type:

interface IFooService {
    aaa(): void;
    search(query: string): any;
}

interface IBarService {
    bbb(): void;
}

interface IAllServices {
    foo: IFooService,
    bar: IBarService,
}

On the front-end there's a generic search component that can search any of the services, and I was trying to get rid of the any type of the input property used to set which service to call.

For this I need to extract all keys on IAllServices, where the type of the key's value has a search method, into a type SearchableServiceKey. I.e. in the above example, IFooService has it, and IBarService does not, so I want the extracted type to be equivalent to type SearchableServiceKey = 'foo'.

The point of the exercise being that the type checker doesn't accept this:

function search(services: IAllServices, service: string, query: string) {
    services[service].search(query);
}

because services[service] only has the empty intersection of the keys of IFooService and IBarService; but should accept this:

function search(services: IAllServices, service: SearchableServiceKey, query: string) {
    services[service].search(query);
}

I managed to get this working using this construction:

type ServiceKey = keyof IAllServices;

interface ISearchable {
    search(query: string): any;
}

type IsSearchable<SK extends ServiceKey> = IAllServices[SK] extends ISearchable
    ? SK
    : never;

type ValuesOf<T> = T[keyof T];

type SearchableServiceKey = Exclude<
    ValuesOf<{ [SK in ServiceKey]: IsSearchable<SK> }>,
    never
>;

but it's kind of a mouthful with the detour through mapping a key to its own type or never to express a predicate. Is there a simpler way to express this in TypeScript?

Upvotes: 1

Views: 108

Answers (1)

jcalz
jcalz

Reputation: 328453

What you've got is correct, and the only strictly unnecessary part is where you manually try to remove never types as constituents of a union. The compiler already automatically eliminates never from unions:

type Foo = never | string | never | number | never | boolean | never;
// IntelliSense shows: type Foo = string | number | boolean;

The only other thing I'd suggest is that you don't need to give type alias names to all the intermediate steps of your type if you're not going to reuse them. The idea of "give me the keys of an object type T whose properties are assignable to a value type V" comes up often enough that I usually define the following type alias:

type KeysMatching<T, V> =
    { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];

And then you can derive the type you're looking for as

type SearchableServiceKey = 
    KeysMatching<IAllServices, {search(q: string): any}>;
// IntelliSense shows: type SearchableServiceKey = "foo"

Playgound link to code

Okay, hope that helps. Good luck!

Upvotes: 3

Related Questions