I have the following interface:
interface MyInterface {
GetChocolate: () => string;
GetVanilla: () => number;
SetChocolate: () => number;
SetVanilla: () => string;
}
The interface is only made up of function types. I have a function that takes in a keyof MyInterface
and does something with it (doesn't matter what). However, I only want the function to accept keys where the return type is string
. I have something like this:
type KeysReturnStrings<K extends keyof MyInterface> = ReturnType<MyInterface[K]> extends string ? K : never;
This works great when I only pass in one key:
type Test1 = KeysReturnStrings<"GetChocolate">; // "GetChocolate"
type Test2 = KeysReturnStrings<"SetChocolate">; // never
But this does not work when I pass in keyof MyInterface
:
type Test3 = KeysReturnStrings<keyof MyInterface>; // never
The type of Test3
when I want it to be "GetChocolate" | "SetVanilla"
.
I can create a different generic type that filters the keys based on if they contain the work "Get"
:
type KeysNoGet<K extends keyof MyInterface> = K extends `Get${string}` ? never : K;
type Test4 = KeysNoGet<keyof MyInterface>; // "SetChocolate" | "SetVanilla"
I just do not understand why Test4
works but Test3
doesn't. My only guess is that the issue is to do with passing keyof MyInterface
into ReturnType<MyInterface[K]>
as this creates a union of the return types of that object (string | number
) which obviously do not extend type string
. But then keyof MyInterface
is a union regardless so it shouldn't be able to extend Get${string}
either.
Is there a utility type or a another generic type that can exclude these keys from the union type?
You want KeysReturnStrings<K>
to distribute over unions in K
, so that KeysReturnStrings<K1 | K2 | K3>
is equivalent to KeysReturnStrings<K1> | KeysReturnStrings<K2> | KeysReturnStrings<K3>
. But your definition does not do so, and you get unexpected results.
There are a few ways to get type functions to distribute over unions; one of them is to use a distributive conditional type where the type you check is a generic type parameter corresponding to the union you want to distribute over. A type of the form type F<T> = T extends U ? X<T> : Y<T>
will distribute over unions in T
. Your KeysNoGet<K>
is of that form, so that's why it behaves as you expect.
So we can wrap your definition of KeysReturnStrings<K>
in a "no-op" distributive conditional type, which always takes the true
branch. We're only using it for its distributive property, not for any sort of conditional type check:
type KeysReturnStrings<K extends keyof MyInterface> = K extends unknown ?
ReturnType<MyInterface[K]> extends string ? K : never
: never;
Now you get the behavior you expect:
type Test1 = KeysReturnStrings<"GetChocolate">;
// ^? type Test1 = "GetChocolate"
type Test2 = KeysReturnStrings<"SetChocolate">;
// ^? type Test2 = never
type Test3 = KeysReturnStrings<keyof MyInterface>;
// ^? type Test3 = "GetChocolate" | "SetVanilla"