So I have this factory pattern where I can create new Instances of my Service Classes with the correct typings. For this I need a type where I extract a single key:
type ServiceSingleKeys<T> = [T] extends (
T extends 'foo' | 'oof' ? [T] : never
)
? T
: never;
The code above is from a code snippet I found, but the snippet isn't explained. I sadly lost the link to the snippet so I cannot provide the link here.
Here is a more detailed reproduction: https://stackblitz.com/edit/typescript-ha1bha?file=index.ts
What does the [T]
do? (Answered in comment --> Tuple)
What effect gets achieved by first checking if [T]
extends (T extends ServiceKeys ? [T] : never)
? How does this work or in other words what is this doing? In my understanding I can achieve the same effect from the type with this
type ServiceSingleKeys<T> = T extends 'foo' | 'oof' ? T : never;
CodePudding user response:
The code
type ServiceSingleKeys<T> = [T] extends (
T extends 'foo' | 'oof' ? [T] : never
) ? T : never;
has the effect of checking whether or not the entire input type T
is assignable to the union type "foo" | "oof"
. If it is, it returns T
. If not, it returns never
. So you get this behavior:
type A = ServiceSingleKeys<"foo" | "oof"> // "foo" | "oof"
type B = ServiceSingleKeys<"foo"> // "foo"
type C = ServiceSingleKeys<"foo" | "bar"> // never
Type A
and B
are the same as their inputs, because "foo" | "oof"
and "foo"
are both assignable to "foo" | "oof"
. type C
is never
, because "foo" | "bar"
is not assignable to "foo" | "oof"
.
This could be rewritten more simply as:
type ServiceSingleKeys<T> = [T] extends ['foo' | 'oof'] ? T : never;
type A = ServiceSingleKeys<"foo" | "oof"> // "foo" | "oof"
type B = ServiceSingleKeys<"foo"> // "foo"
type C = ServiceSingleKeys<"foo" | "bar"> // never
And indeed, this is how I would have written such a type if I needed to. I cannot say why the original version is written how it is. My guess would be that it was assembled via trial and error. (If it turns out that there is some edge case where these two versions are not functionally equivalent, then maybe that edge case is the reason, but without more information I'm skeptical).
You cannot remove the tuple type wrapper from the check without changing the behavior:
type NotServiceSingleKeys<T> = T extends 'foo' | 'oof' ? T : never;
type A = NotServiceSingleKeys<"foo" | "oof"> // "foo" | "oof"
type B = NotServiceSingleKeys<"foo"> // "foo"
type C = NotServiceSingleKeys<"foo" | "bar"> // "foo" <-- difference
That's because a conditional type in which the checked type is a bare generic type parameter is a distributive conditional type, where the input type is broken up into its individual union members before being evaluated, and the output is joined into a new union. So NotServiceSingleKeys<"foo" | "bar">
is evaluated as NotServiceSingleKeys<"foo"> | NotServiceSingleKeys<"bar">
which becomes "foo" | never
or just "foo"
.
Distributive conditional types are often what people want to see (this allows you to get union-filtering behavior like the Extract<T, U>
utility type), but when it is undesirable, then the fix is usually to wrap both sides of the extends
check in one-tuples. The check [T] extends ['foo' | 'oof']
is non-distributive because [T]
is not a bare generic type parameter, but the check is similar because tuples are covariant (meaning that [XXX]
is assignable to [YYY]
if and only if XXX
is assignable to YYY
). As it says in the above-linked documentation:
Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the
extends
keyword with square brackets.
Again, my guess here is that the original version of the code was a trial-and-error attempt to apply that advice in order to prevent the check from being accidentally distributive. If there is some other purpose to the particular nested conditional type in the question, I don't see it.