2

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.

Lev Izraelit
  • 512
  • 4
  • 14
  • I'd love to know what use case would require a union type and forbid a non-union type. Especially since what TypeScript considers a union is often different from what users of TypeScript expect (e.g., `boolean` is a union, and `"a" | "b" | "c" | string` is not a union). – jcalz Sep 03 '21 at 02:03

2 Answers2

7

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

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • I tried using the above approach to force the generic type to support a specific string. But TypeScript still doesn't allow the code to assign that string to the type. Any ideas? Here's a contrived example: `const defaultValue = (value?: T): T => value || "Unavailable";` – Michael Best Aug 05 '23 at 00:23
  • @MichaelBest You don't need fancy conditional types or lower bounds, just write `(value?: T | "Unavailable"): T | "Unavailable" => value || "Unavailable"`. – kaya3 Aug 05 '23 at 00:32
0

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");
Invizi
  • 1,270
  • 1
  • 7
  • 7
  • This would still allow calling it like `onlyUnions("foobar")` where the type is just `string`. – kaya3 Sep 02 '21 at 23:19