Discriminated Union

If you have a class with a literal member then you can use that property to discriminate between union members.

As an example consider the union of a Square and Rectangle, here we have a member kind that exists on both union members and is of a particular literal type:

  1. interface Square {
  2. kind: "square";
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: "rectangle";
  7. width: number;
  8. height: number;
  9. }
  10. type Shape = Square | Rectangle;

If you use a type guard style check (==, ===, !=, !==) or switch on the discriminant property (here kind) TypeScript will realize that the object must be of the type that has that specific literal and do a type narrowing for you :)

  1. function area(s: Shape) {
  2. if (s.kind === "square") {
  3. // Now TypeScript *knows* that `s` must be a square ;)
  4. // So you can use its members safely :)
  5. return s.size * s.size;
  6. }
  7. else {
  8. // Wasn't a square? So TypeScript will figure out that it must be a Rectangle ;)
  9. // So you can use its members safely :)
  10. return s.width * s.height;
  11. }
  12. }

Exhaustive Checks

Quite commonly you want to make sure that all members of a union have some code(action) against them.

  1. interface Square {
  2. kind: "square";
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: "rectangle";
  7. width: number;
  8. height: number;
  9. }
  10. // Someone just added this new `Circle` Type
  11. // We would like to let TypeScript give an error at any place that *needs* to cater for this
  12. interface Circle {
  13. kind: "circle";
  14. radius: number;
  15. }
  16. type Shape = Square | Rectangle | Circle;

As an example of where stuff goes bad:

  1. function area(s: Shape) {
  2. if (s.kind === "square") {
  3. return s.size * s.size;
  4. }
  5. else if (s.kind === "rectangle") {
  6. return s.width * s.height;
  7. }
  8. // Would it be great if you could get TypeScript to give you an error?
  9. }

You can do that by simply adding a fall through and making sure that the inferred type in that block is compatible with the never type. For example if you add the exhastive check you get a nice error:

  1. function area(s: Shape) {
  2. if (s.kind === "square") {
  3. return s.size * s.size;
  4. }
  5. else if (s.kind === "rectangle") {
  6. return s.width * s.height;
  7. }
  8. else {
  9. // ERROR : `Circle` is not assignable to `never`
  10. const _exhaustiveCheck: never = s;
  11. }
  12. }

That forces you to handle this new case :

  1. function area(s: Shape) {
  2. if (s.kind === "square") {
  3. return s.size * s.size;
  4. }
  5. else if (s.kind === "rectangle") {
  6. return s.width * s.height;
  7. }
  8. else if (s.kind === "circle") {
  9. return Math.PI * (s.radius **2);
  10. }
  11. else {
  12. // Okay once more
  13. const _exhaustiveCheck: never = s;
  14. }
  15. }

Switch

TIP: of course you can also do it in a switch statement:

  1. function area(s: Shape) {
  2. switch (s.kind) {
  3. case "square": return s.size * s.size;
  4. case "rectangle": return s.width * s.height;
  5. case "circle": return Math.PI * s.radius * s.radius;
  6. default: const _exhaustiveCheck: never = s;
  7. }
  8. }

strictNullChecks

If using strictNullChecks and doing exhaustive checks you should return the _exhaustiveCheck variable (of type never) as well, otherwise TypeScript infers a possible return of undefined. So:

  1. function area(s: Shape) {
  2. switch (s.kind) {
  3. case "square": return s.size * s.size;
  4. case "rectangle": return s.width * s.height;
  5. case "circle": return Math.PI * s.radius * s.radius;
  6. default:
  7. const _exhaustiveCheck: never = s;
  8. return _exhaustiveCheck;
  9. }
  10. }

Redux

A popular library that makes use of this is redux.

Here is the gist of redux with TypeScript type annotations added:

  1. import { createStore } from 'redux'
  2. type Action
  3. = {
  4. type: 'INCREMENT'
  5. }
  6. | {
  7. type: 'DECREMENT'
  8. }
  9. /**
  10. * This is a reducer, a pure function with (state, action) => state signature.
  11. * It describes how an action transforms the state into the next state.
  12. *
  13. * The shape of the state is up to you: it can be a primitive, an array, an object,
  14. * or even an Immutable.js data structure. The only important part is that you should
  15. * not mutate the state object, but return a new object if the state changes.
  16. *
  17. * In this example, we use a `switch` statement and strings, but you can use a helper that
  18. * follows a different convention (such as function maps) if it makes sense for your
  19. * project.
  20. */
  21. function counter(state = 0, action: Action) {
  22. switch (action.type) {
  23. case 'INCREMENT':
  24. return state + 1
  25. case 'DECREMENT':
  26. return state - 1
  27. default:
  28. return state
  29. }
  30. }
  31. // Create a Redux store holding the state of your app.
  32. // Its API is { subscribe, dispatch, getState }.
  33. let store = createStore(counter)
  34. // You can use subscribe() to update the UI in response to state changes.
  35. // Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
  36. // However it can also be handy to persist the current state in the localStorage.
  37. store.subscribe(() =>
  38. console.log(store.getState())
  39. )
  40. // The only way to mutate the internal state is to dispatch an action.
  41. // The actions can be serialized, logged or stored and later replayed.
  42. store.dispatch({ type: 'INCREMENT' })
  43. // 1
  44. store.dispatch({ type: 'INCREMENT' })
  45. // 2
  46. store.dispatch({ type: 'DECREMENT' })
  47. // 1

Using it with TypeScript gives you safety against typo errors, increased refactor-ability and self documenting code .