Constraining a generic parameter to be a union type in Typescript

Viewed 1313

Is there a way in Typescript to constrain a generic parameter to be a union type? To illustrate my intention, below I pretend T extends UnionType will achieve this:

function doSomethingWithUnion<T extends UnionType>(val: T) {}

doSomethingWithUnion<number | string>(...) // good
doSomethingWithUnion<number>(...) // should result in an error

I've found in another SO question (53953814) that we can check if a type is a union type:

type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true

type Foo = IsUnion<string | number> // true
type Bar = IsUnion<string> // false

Which allows to, for example, to assert than a function will never return if a generic parameter is not a union type:

declare function doSomethingWithUnion<T>(val: T): [T] extends [UnionToIntersection<T>] ? never: boolean;

doSomethingWithUnion<number>(2) // never
doSomethingWithUnion<string|number>(2) // => boolean

(Link to playground)

However, I have not found a way to constrain the type itself in a similar manner.

2 Answers

Typescript only allows you to specify an upper bound for a generic type parameter, not a lower bound or any other constraint. So it seems like it should be impossible, and at first I thought it must be. But it turns out this is possible, by making T's upper bound depend on T itself.

function test<T extends (IsUnion<T> extends true ? any : never)>(arg: T): T {
  // ...
  return arg;
}

// OK
test<string | number>('foobar');

// Error: Type 'string' does not satisfy the constraint 'never'.
test<string>('foobar');

// Error: Argument of type 'string' is not assignable to parameter of type 'never'.
test('foobar');

The one caveat I can think of is that T can always be never, even though that is not a union. That's unavoidable because never extends everything, so it extends any upper bound that the type variable could have. But if T is never then the function can only be called with an argument of type never (i.e. it can never be called), so this shouldn't be a problem in practice.

If you need another upper bound for T, you can write that instead of any.

Playground Link

I tried this, and seems to work. Not sure if I can simplify it by passing a union type instead of two types that get put into a union.

function onlyUnions<T, I>(result: T | I) {
    return result;
}

onlyUnions<string, number>("string");
Related