0

The thing I'm trying to achieve is repeating myself less and have less bug-potential.

I need the following (yes I need both):

const unionTypes = [GQLDualTaskState, GQLNumericTaskState];
type UnionType = GQLDualTaskState | GQLNumericTaskState;

As you can see I have to repeat the classes and change always both (e.g. adding or removing a class).

I searched quite a bit to find a solution by myself but didn't. This seem to work with strings only: https://stackoverflow.com/a/45486495/1469540

The step I'm currently at is:

type UnionTypeDef = [GQLDualTaskState, GQLNumericTaskState];
const unionTypes = [GQLDualTaskState, GQLNumericTaskState];
type UnionType = UnionTypeDef[number];

I understand that the syntax Class['property'] returns the type of the property property of the class Class. But why is the result of UnionTypeDef[number] a Union?

Summarized there are two questions:

  1. How do I solve the problem of having a list of classes and a union of the same classes (and I want to understand the solution)?
  2. Why is the result of UnionTypeDef[number] a Union?
nidomiro
  • 820
  • 2
  • 10
  • 24

1 Answers1

1

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

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • Thank you so much :) `InstanceType` was the missing piece. And thank you for explaining the result. It helped me a lot in understanding TS better. I'm coming from Java/Kotlin and therefore have some "understanding" difficulties with the TS-typesystem. Do you have any link for me to learn the typesystem in more detail (deep-dive)? – nidomiro Jan 22 '21 at 17:01
  • 1
    The official typescript handbook is good: https://www.typescriptlang.org/docs/handbook/intro.html The rest just comes with experience (and lots of googling) – Alex Wayne Jan 22 '21 at 18:51