Reputation: 39960
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
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"
Okay, hope that helps. Good luck!
Upvotes: 3