Why is the result of UnionTypeDef[number]
a Union?
Why wouldn't it be?
See the following comments explaining exactly what happening here.
class A { a: string = 'a' }
class B { b: string = 'b' }
// This is a type. It is tuple with a length of 2.
// index 0: a class A instance
// index 1: a class B instance
type UnionTypeDef = [A, B];
// This is a value. It is an array with a length of 2.
// index 0: the class A constructor
// index 1: the class B constructor
const unionTypes = [A, B];
// When you index an array or tuple by `number` you get a union of all possible
// values of all indices. In this case, `A | B`.
type UnionType = UnionTypeDef[number];
How do I solve the problem of having a list of classes and a union of the same classes?
First of all, you say these are classes. So that would means that const unionTypes
are the class constructors, and you would use type UnionType
for instances?
If so, you want to start with a constant tuple [A, B, C]
rather than the type. This is because types do not exist at runtime, so you can't create a value from a type, but you can generate a type from a value.
class A { a: string = 'a' }
class B { b: string = 'b' }
class C { c: string = 'c' }
const arrayOfConstructors = [A, B]; // only include A, and B
// Solution:
type UnionType = InstanceType<(typeof arrayOfConstructors)[number]>;
// UnionType can be instance of A or B
const instanceOfA: UnionType = new A()
const instanceOfB: UnionType = new B()
const instanceOfC: UnionType = new C() // Error: Type 'C' is not assignable to type 'A | B'.
Here's how that works:
typeof arrayOfConstructors
In order to get a type from a value, you need to use the typeof
operator. This brings it into type land.
(typeof arrayOfConstructors)[number]
To get a union of all possible members of an array or tuple, you index it by number. All possible values for all numeric keys are returned as a union. i.e. ['a', 'b'][number] //=> 'a' | 'b'
InstanceType<SomeClass>
When using a class as a type, typescript assumes you mean the type for instances of this class. If you mean the class constructor, then you use typeof MyClass
instead.
So const arrayOfConstructors = [A, B]
creates an array of class constructors, not instances. Which means the type of that value would actually be [typeof A, typeof B]
. InstanceType
gets the type of instances from a class type.
For example: (a very contrived example)
type ABInstances = InstanceType<typeof A | typeof B> // type is A | B
All that means that, since you have a union of class constructors, you need to get the instance types from them.
If you use the pattern a lot, you can refactor it to something like:
interface Constructable { new(...args: any[]): any }
type InstanceUnion<T extends Constructable[]> = InstanceType<T[number]>
The Constructable
type represents any class constructor. It defined so that it can be used a constraint in the InstanceUnion
, thereby throwing type errors you you try to pass it a string or plain object.
The InstanceUnion
type accepts an array or tuple of class constructors, gets the union of its values, and then gets the InstanceType
of all members of that union.
Now you can just:
type UnionType = InstanceUnion<typeof arrayOfConstructors>;
type OtherUnionType = InstanceUnion<typeof otherArrayOfConstructors>;
Playground with working examples