Typescript generic parameters that extends exclusive unions

Viewed 983

In TypeScript, is there a way to define a generic parameter that extends only one value of a union type?

For example, assuming I declare a union type as follows:

type Any = "A" | "B"

then if I use the type in a function definition as follows:

const fn = <T extends Any>(arg: T[]) => {}

then the args argument can be an array that contain the values of both "A" and "B"; for example, this would be valid:

let x = fn(["A", "B"])

which defeats the very purpose of using the generic parameter in the function definition (i.e., to constrain the values in args argument array to only one specific type)

Of course, I could define the function as follows:

const fn = (arg: "A"[] | "B"[]) => {}

But if the number of component types in the union is large, this may be impractical

2 Answers

There is a proposed feature that would allow you to tell the compiler that T is one of a number of types instead of a union of them. The issue is marked as in discussion, so maybe add a +1 for it.

In the meantime we can force the compiler to give us an error if T is a union using conditional types:

type Any = "A" | "B"

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type NoUnion<T, TError> = [T] extends [UnionToIntersection<T>] ? {} : TError

type d = NoUnion<Any, ""> 
const fn = <T extends Any>(arg: T[] & NoUnion<T, "Must be A or B not a union">) => { }

fn(null as "A"[])
fn(null as "B"[])
fn(null as ("A" | "B")[]) //error Type 'Any[]' is not assignable to type '"Must be A or B not a union"'

Here is a solution and it is very straightforward. I've spiced it up by adding a return type also - get that over with.

type Any = "A" | "B" | "C" | "D" | "E"
type Fn<T> = (arg:T[])=>T

// This distributive conditional operator maps the 
// union of elements-of-Any to a union-of-functions with exclusive Parameters 
type FnAny_<T> = T extends any ? Fn<T> : never;
type FnAny = FnAny_<Any>

// Obviously you have to define the real content of `fnAny` 
// to correctly match the definition - this is a dummy defn
function fnAny<T extends FnAny>(...a:Parameters<T>):ReturnType<T>{
  if (Math.random()<0.5)
    return undefined as unknown as ReturnType<T>
  else 
    return undefined as unknown as ReturnType<T>
}
fnAny(["A","A"]) // ok
fnAny(["A","E"]) // ERROR - rejected by type checker


This solution works on the Typescript Playground for all available versions from 3.3.3333 to the latest 4.5.4.

The error message:

  • Argument of type '[("A" | "E")[]]' is not assignable to parameter of type '[arg: "A"[]] | [arg: "B"[]] | [arg: "C"[]] | [arg: "D"[]] | [arg: "E"[]]'.
  • Type '[("A" | "E")[]]' is not assignable to type '[arg: "E"[]]'.
  • Type '("A" | "E")[]' is not assignable to type '"E"[]'.
  • Type '"A" | "E"' is not assignable to type '"E"'.
  • Type '"A"' is not assignable to type '"E"'.
Related