Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: James Morrow
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: James Morrow

NgRx Store Reducers Work Using A TypeScript Feature Called A Discriminated Union

By
Published in

Lately, I've been trying to understand how, when, and where to use a store-based state management approach like NgRx Store (or Redux, or MobX, etc). I'm currently 75% of the way through Todd Motto's NgRx Store course; and, I've recently raved about Angular University's article, NgRx Store - An Architecture's Guide. Slowly, it's coming together in my mind. But, there was one feature of the NgRx action reducers that I didn't understand: how TypeScript was performing type validation based on the switch(action.type) control flow. After doing a bit of reading, I discovered that the reducer code is using a TypeScript feature known as a "Discriminated Union".

A while back, I learned about "Type Guards" in TypeScript. Type Guards allow you to safely access properties on a given value if you are inside a code-path that has validated the type of the given value. A Discriminated Union is a Type Guard related feature built on top of TypeScript's Type unions. In a Discriminated Union, the type of each definition in the union can be determined by a unique value known as the "discriminator".

To see what I mean, I've tried to boil the concept down to something that looks like an action reducer in NgRx store. In this demo, I have three Class definitions that all have a "readonly" property, "type". This "type" property will act as the "discriminator" in our Discriminated Union. It is important that this property be "readonly" so that TypeScript knows that it will be a constant and consistent indicator of the class. If the property is not "readonly", TypeScript will complain:

// NOTE: When we are using the "type" property as a "discriminator", it has to be defined
// as "readonly" so that TypeScript knows that the property is a constant predicator of
// the class shape. If it weren't "readonly", it could change during the life-time of the
// application, at which point it would no longer be dependable.
class ActionOne {
	public readonly type = "ActionOne"; // Discriminator.
	public valueOne = "I am action one value!";
}

class ActionTwo {
	public readonly type = "ActionTwo"; // Discriminator.
	public valueTwo = "I am action Two value!";
}

class ActionThree {
	public readonly type = "ActionThree"; // Discriminator.
	public valueThree = "I am action Three value!";
}

// A discriminated union is just a regular type union that will be consumed later on
// based on the discriminator field (ie, "type" property in our case).
type DiscriminatedUnionForActions = ActionOne | ActionTwo | ActionThree;

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

function handleAction( action: DiscriminatedUnionForActions ) : string {

	// Since TypeScript knows that this "action" argument is one of the types in our
	// discriminated union, we can use the discriminating property (type) to access a
	// class-specific property that is not [necessarily] shared among the different types.
	switch ( action.type ) {
		case "ActionOne":
			return( action.valueOne );
		break;
		case "ActionTwo":
			return( action.valueTwo );
		break;
		case "ActionThree":
			return( action.valueThree );
		break;
	}

}

console.log( handleAction( new ActionTwo() ) );

As you can see, each of my there Action classes has a public, readonly property "type". And, each class has a unique secondary property (valueOne, valueTwo, or valueThree). Inside our "reducer" function - handleAction() - we look at the "type" property of the given action using a switch statement. This type-property-based control flow then allows TypeScript to perform type validation within the subsequent "case" statements.

In the above example, I mentioned that the "type" property has to be "readonly" so that TypeScript knows that it can depend on it as a discriminator. However, if we don't use a class, we can still achieve the same functionality using interfaces with string-literal members. As Basarat points out in his TypeScript GitBook chapter on Discriminated Unions, interfaces that have a property defined as a constant string value can also be used in the same kind of control flow.

To see this in action, I've tried to recreate the first demo using interfaces:

// NOTE: When we are using the "type" property as a "discriminator", it has to be a
// constant and consistent indicator of type. Since we don't have the "readonly" concept
// here in the Interface definitions, we can create the same effect by defining each
// "type" property as a string-literal. This will indicate to TypeScript that this "type"
// property will not change during the life-time of the application.
interface ActionOne {
	type: "ActionOneType";
	valueOne: string;
}

interface ActionTwo {
	type: "ActionTwoType";
	valueTwo: string;
}

interface ActionThree {
	type: "ActionThreeType";
	valueThree: string;
}

type DiscriminatedUnionForActions = ActionOne | ActionTwo | ActionThree;

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// NOTE: TypeScript complains if I don't explicitly type the variable (ie, :ActionTwo).
var value: ActionTwo = {
	type: "ActionTwoType",
	valueTwo: "What it be like?"
};

console.log( handleAction( value ) );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

function handleAction( action: DiscriminatedUnionForActions ) : string {

	// Since TypeScript knows that this "action" argument is one of the interfaces in
	// our discriminated union, we can use the discriminating property (type) to access
	// a class-specific property that is not [necessarily] shared among the different
	// interfaces.
	switch ( action.type ) {
		case "ActionOneType":
			return( action.valueOne );
		break;
		case "ActionTwoType":
			return( action.valueTwo );
		break;
		case "ActionThreeType":
			return( action.valueThree );
		break;
	}

}

In this case, instead of using a "readonly" property on a Class definition, I'm using a string-literal property on an Interface definition. In both cases, we're indicating to TypeScript that the property will remain fixed throughout the life-time of the application. And, as such, TypeScript can use the property as a discriminator in our switch-based Type Guard.

Anyway, this was mostly a "note to self" kind of post. But, I figured I'd share this information since I know State Stores are all the rage these days. And, anyone who's building theirs using TypeScript might not understand why TypeScript actually allows it to compile. This certainly cleared it up in my mind.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel