Home > front end >  How to check if tuple includes a particular type inside a conditional type?
How to check if tuple includes a particular type inside a conditional type?

Time:07-06

As of TypeScript 4.2, it's trivial to check if a tuple type begins or ends with a specific type:

type HeadIsString<T extends any[]> = T extends [string, ...any[]] ? true : false;

type TailIsString<T extends any[]> = T extends [...any[], string] ? true : false;

However, I'd like to check if a tuple includes a particular type at any position in the tuple. Something like this is not currently possible

type IncludesString<T extends any[]> = T extends [...any[], string, ...any[]] ? true : false;

because a tuple cannot have more than one rest element. The only solution I can think of is to turn the tuple into a union first.

type IncludesString<T extends any[]> = string extends T[number] ? true : false;

My specific use case is, given an array of functions, knowing if any of them return a Promise. For this specific use case, the above solution is sufficient. However, I could see other scenarios where this approach would not work. For example, where tuple elements could themselves be unions, but a type like string | null would be undesirable. So in this example, [number, string | null] would be transformed into number | string | null and the type would evaluate to true.

To clarify, I'm looking for a type that would, in our example, evaluate as follows:

IncludesString<[]> // false
IncludesString<[number]> // false
IncludesString<[[string]]> // false
IncludesString<[string, number]> // true
IncludesString<[number, string, number]> // true

The use of string in this example is arbitrary -- I'm just looking for a generic approach that would avoid the aforementioned issue with using a union.

CodePudding user response:

Here is one possible approach:

type IncludesString<T extends any[]> =
    { [I in keyof T]: T[I] extends string ? unknown : never }[number] extends
    never ? false : true;

The idea is that we want to map the tuple type T to another tuple, where we turn anything extending string into the unknown "top" type, and anything not extending string into the never "bottom" type. So, for example, [string, number, string | number, "abc"] would be transformed into [unknown, never, never, unknown], since both string and "abc" extend string, while both number and string | number do not.

Once we have that mapped tuple, we index into it with number to get the union of all the elements of the mapped tuple. In the previous example, that's [unknown, never, never, unknown][number], which becomes unknown | never | never | unknown, which becomes unknown. TypeScript immediately collapses any union of the form A | unknown to unknown, and any union of the form A | never to A. That means when you union together a bunch of types which are either unknown or never, the outcome will be unknown if at least one of the input types is unknown, and the outcome will be never if none of the input types is unknown.

And because we have transformed the tuple so that anything extending string becomes unknown and anything not extending string becomes never, the resulting union will be unknown if and only if there's at least one member of the original tuple that extends string.

So now all we have to do is map unknown to true and never to false. We do that with extends never ? false : true (since unknown extends never is false, and never extends never is true).


Let's test it out:

type X0 = IncludesString<[]> // false
type X1 = IncludesString<[number]> // false
type X2 = IncludesString<[[string]]> // false
type X3 = IncludesString<[string, number]> // true
type X4 = IncludesString<[number, string, number]> // true
type X5 = IncludesString<[string | number]>; // false
type X6 = IncludesString<["abc"]> // true
type X7 = IncludesString<[string] | [number]> // true?

Most of those are exactly the output you wanted. The last one, though, IncludesString<[string] | [number]> is true, and it's not clear if that's what you want. Perhaps you want true | false as the output. Or maybe false. Who knows? There are always plenty of edge cases involved in custom type functions like this, and the only way to deal with that is to test thoroughly against your actual use cases and tweak the definition accordingly. But I consider that outside the scope of the question as asked.

Playground link to code

  • Related