Introduction to Correlated Union Types in TypeScript
In TypeScript, a union type is a powerful way to express a value that can be one of several types. Two or more data types are combined using the pipe symbol (|) to denote a Union Type[8]. For instance, a variable can be defined as a string or a number using a union type, like so: let code: (string | number);
[5].
However, when dealing with more complex scenarios, such as strictly typed event emitters, we may encounter the need for correlated union types. Correlated union types are a concept where the types of different properties of an object are linked, or correlated, in some way[1].
Correlated Union Types in Practice
Consider the following example of a union type in TypeScript:
type NumberRecord = { kind: "n", v: number, f: (v: number) => void };
type StringRecord = { kind: "s", v: string, f: (v: string) => void };
type BooleanRecord = { kind: "b", v: boolean, f: (v: boolean) => void };
type UnionRecord = NumberRecord | StringRecord | BooleanRecord;
In this example, the kind
property determines the types of the v
and f
properties. This is a form of correlation between the types. However, TypeScript currently does not support this kind of correlation directly. If you try to call the function f
with the value v
in a function that takes a UnionRecord
, TypeScript will give an error[1].
Strictly Typed Event Emitter Using Correlated Union Types
Let's consider a more practical example: a strictly typed event emitter. In this case, we have different events that each have a different type of payload. We want to ensure that the correct payload type is used for each event. Here's an example of how this might be done using correlated union types:
type PayloadOneArg<T> = undefined extends T ? [T?] : [T];
type PayloadArgs<T> = T extends undefined ? [] : PayloadOneArg<T>;
type Listener<Arguments> = (...args: PayloadArgs<Arguments>) => void;
interface EventEmitter<T> {
on<K extends keyof T>(eventName:K, listener: Listener<T[K]>): void;
}
interface PayloadA { readonly foo: number; readonly bar: string; }
interface PayloadB { readonly foo: number; readonly baz: boolean; }
type EmitterA = EventEmitter<PayloadA>;
type EmitterB = EventEmitter<PayloadB>;
interface ModuleA { readonly type: 'a'; readonly on: EmitterA['on']; }
interface ModuleB { readonly type: 'b'; readonly on: EmitterB['on']; }
function doStuff(mod: ModuleA | ModuleB): void {
if (mod.type === 'a') {
mod.on('foo', () => {}); // works
} else {
mod.on('foo', () => {}); // works
}
mod.on('foo', () => {}); // doesn’t work
}
In this example, the on
method of the EventEmitter
interface takes an event name and a listener. The listener is a function that takes arguments of a type determined by the event name. This is a form of correlation: the type of the listener's arguments is correlated with the event name[4].
Conclusion
Correlated union types can be a powerful tool for creating more precise type definitions in TypeScript. While TypeScript does not currently support them directly, they can be emulated using existing TypeScript features. However, this can sometimes lead to more complex and less intuitive type definitions. Despite these challenges, correlated union types can be very useful in certain scenarios, such as creating strictly typed event emitters[4].
Citations: [1] https://github.com/microsoft/TypeScript/issues/30581 [2] https://www.w3schools.com/typescript/typescript_union_types.php [3] https://spin.atomicobject.com/2019/03/25/disjoint-unions-typescript-conditional-types/ [4] https://stackoverflow.com/questions/73246386/how-to-use-correlated-union-types-for-a-strictly-typed-event-emitter [5] https://www.tutorialsteacher.com/typescript/typescript-union [6] https://css-tricks.com/typescript-discriminated-unions/ [7] https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html [8] https://www.tutorialspoint.com/typescript/typescript_union.htm [9] https://felt.com/blog/narrowing-typescript-type-predicates-discriminated-unions [10] https://kevinwil.de/enforcing-correlated-types/ [11] https://camchenry.com/blog/typescript-union-type [12] https://blog.logrocket.com/understanding-discriminated-union-intersection-types-typescript/ [13] https://www.typescripttutorial.net/typescript-tutorial/typescript-union-type/ [14] https://typescript-site-76.ortam.vercel.app/docs/handbook/advanced-types.html [15] https://javascript.plainenglish.io/case-study-a-practical-usage-of-typescript-discriminated-union-type-and-generics-87e75a2717f8