Tagged unions are a way of saying "this data is any one of these values", and the tag is a property that discriminates the values. It enables some really powerful type system features, like exhaustive checking and control flow analysis.
Tagged unions also go by the name of: tagged unions, discriminated unions, algebraic data types (ADTs), and sum types. Just to confuse matters.
In many functional languages this is a really common pattern, like Haskell, Elm, PureScript, and Scala. So much so, they have their own native syntax.
For example, in Elm we can define a tagged union representing an anonymous or named user with:
// elm
type User = Anonymous | Named String
… where Anonymous
and Named
are the tags that differentiate the values, and String
is the type of the value you'll get for Named
. This also creates constructors for Anonymous
and Named
.
We can do the same in TypeScript, but there's a lot of boilerplate. Most often, you have to define an enum or string literals for the tags, and then constructors and types for each record in the union:
// plain TS:
// Union tag
enum UserType {
Anonymous,
Named,
}
// Records and constructors
type Anonymous = {
type: UserType.Anonymous;
value: {}
};
type Named = {
tag: UserType.Named;
value: {
name: string;
};
};
const createAnonymous = (): Anonymous => ({ type: UserType.Anonymous, value: {} });
const createNamed = ({ name }: { name: string }): Named => ({
tag: UserType.Named,
value: { name },
});
// Tagged union
type User = Anonymous | Named;
Unionize is a fantastic little library that abstracts away much of this boilerplate:
// with unionize:
import { unionize, ofType } from 'unionize';
const User = unionize({
Anonymous: ofType<{}>(),
Named: ofType<{ name: string }>(),
});
type UserUnion = typeof User._Union;
// example of using constructors:
User.Anonymous({});
User.Named({ name: 'foo' });