Introduction

Variant aims to bring the experience of variant types to TypeScript. Variant types, a.k.a. discriminated unions in the TypeScript world, are an excellent tool for describing and handling flexible domain models and tiny DSLs. However, because "TypeScript instead builds on JavaScript patterns as they exist today" using them as-is can result in tedious and fragile code. This project addresses that by providing well-typed, fluent, and expressive tools to safely do away with the boilerplate.

๐Ÿง  Click here to jump straight to the API Reference.

Quick Start

Let's use variant to describe a domain โ€” Animals. Or if you'd like a redux example...

For this application, we care about dogs, cats, and snakes. We have different concerns for each animal, so we'll want to define them with distinct fields. The fields function below is shorthand to help do this. We'll see more of how it works in the first section of the User Guide.

import variant, {variantList, VariantOf, fields, TypeNames} from 'variant';
export const Animal = variantList([
variant('dog', fields<{name: string, favoriteBall?: string}>()),
variant('cat', fields<{name: string, daysSinceDamage: number}>()),
variant('snake', (name: string, patternName?: string) => ({
name,
pattern: patternName ?? 'striped',
})),
]);
export type Animal<T extends TypeNames<typeof Animal> = undefined> = VariantOf<typeof Animal, T>;

We can now import and use the Animal object, which simply collects the tag constructors we care about in one place. To create a new dog, for example, call Animal.dog({name: 'Guava'}). When we imported the Animal object we also imported the Animal type since we defined these with the same name. This single import will allows us to:

  • Create a new animal
    • Animal.snake('steve') โ€” value: { type: 'snake', name: 'steve', pattern: 'striped' }
  • Annotate the type for a single animal.
    • Animal<'snake'> โ€” type: { type: 'snake', name: string, pattern: string }
  • Annotate the union of all animals.
    • Animal โ€” type: Animal<'dog'> | Animal<'cat'> | Animal<'snake'>
import {Animal} from '...';
const snek = Animal.snake('steve');
const describeSnake = (snake: Animal<'snake'>) => {...}
const describeAnimal = (animal: Animal) => {...}

With these building blocks we're ready to write some elegant code. Let's expand the describeAnimal function with the match utility.

Match

Match is a great tool to process a variant of unknown type. The function will accept an variant object (animal) and a handler object. Think of each entry of the handler like a branch that might execute. To be safe the object will need an entry for every case of the variant. In this example that means one for each animal.

import {match} from 'variant';
const describeAnimal = (animal: Animal) => match(animal, {
cat: ({name}) => `${name} is sleeping on a sunlit window sill.`,
dog: ({name, favoriteBall}) => [
`${name} is on the rug`,
favoriteBall ? `nuzzling a ${favoriteBall} ball.` : '.'
].join(' '),
snake: s => `Hi ${s.name}, your ${s.pattern} skin looks nice today.`,
});

If any of this looks unfamiliar, this sample leverages the ES6 lambda expressions, template strings, and parameter destructuring features.


match is...

  • exhaustive by default. If you only need to handle some cases, use partialMatch.
  • pure TypeScript. This will work on any valid discriminated union, whether or not it was made with variant.
  • well typed. match's return type is the union of the return types of all the potential handler functions. partialMatch does the same but adds undefined to the union.
  • familiar. It's meant to imitate the OCaml / Reason ML match statement.
  • flexible. By default match switches on the type property which can be overridden using the function's optional third paramater.

Grouping

Earlier we defined Animal using the variantList function. It's also valid to construct the Animal object directly.

const Animal = {
dog: variant('dog', ...),
cat: variant('cat', ...),
snake: variant('snake', ...),
}

This is discussed further in the page on grouping variants.

Continued

There's more to come. The next page, Motivation, is context and can be skipped. It explains why variant matters and what a vanilla TypeScript approach would look like. The Usage Guide goes over the practical things you need to know and is the next place I'd look as a new user wanting to get things done. Finally, the API Reference is available for details on every function and type.