Skip to content

Latest commit

 

History

History
1328 lines (967 loc) · 44.6 KB

Documentation.md

File metadata and controls

1328 lines (967 loc) · 44.6 KB

free-types

free-types | Use Cases | Documentation | Guide | Algebraic data types

Documentation

Creation | Helpers | Application | Decomposition | Composition | Adapters | Higher order types | Operators

Foreword

In this documentation the types Mappable and NonEmptyMappable appear often. I use them to mean array and object literal types. The conflation of arrays and objects in one type is useful because no type definition can reject empty objects and thus expecting a NonEmptyMappable will force you to check that the input is not empty regardless of it being an array or an object.

Creation

typedescription
Type<⁠Input?, Output?>

Extend Type with an interface to define a free type.

interface $Foo extends Type {
    type: 'foo'
}

Access arguments with this:

interface $Foo extends Type {
    type: this[0]
}

Define constraints on the inputs:

// variadic types with no constraint
interface $Foo extends Type { ... }
interface $Foo extends Type<number> { ... }
interface $Foo extends Type<unknown[]> { ... }

// Setting the arity:
interface $Foo extends Type<1> { ... }

// Setting the arity + a specific constraint:
interface $Foo extends Type<[number, number]> { ... }

// optional parameters:
interface $Foo extends Type<[number, number?]> { ... }
  
// equivalent to the former:
interface $Foo extends Type {
    constraints: [number, number?]
    ...
}

Define constraints on the output:

interface $Foo extends Type<unknown[], string> {
    type: string
}

Public fields:

  • this['arguments']: a copy of the arguments
  • $T['type']: the return type
  • $T['constraints']: the type constraints
Const<⁠T>

Turn a type T into a constant Type<❋, T>, totally ignoring arity and type constraints on the input.

type Foo = MapOver<[1, 2, 3], Const<'A'>>; // [A, A, A]
type Bar = MapOver<[1, 2, 3], $Next>; // [2, 3, 4]
$Const

A free version of Const

type Consts = MapOver<[1,2,3], $Const>
//   Consts : [Type<❋, 1>, Type<❋, 2>, Type<❋, 3>]
From<⁠T, Args?>

Create a new Type based upon a mappable type T using the tuple Args to determine which field to turn into a parameter and in what order they must be applied.

type $Foo = From<{ a: number, b: string, c: boolean }, ['b', 'a']>;
type Foo = apply<$Foo, ['a', 1]>
//   Foo : { a: 1, b: "a", c: boolean }

type $Bar = From<[number, string, boolean], [1, 0]>;
type Bar = apply<$Bar, ['a', 1]>
//   Bar : [1, "a", boolean]

The values of T are used as type constraints:

type Rejected = apply<$Foo, [1, 'a']>
//                          --------
type Rejected = apply<$Bar, [1, 'a']>
//                          --------
// [1, "a"] doesn't satisfy the constraint [string, number]

Optinal Args

When Args is omitted, the resulting free type should take as many arguments as there are fields in T in the same order as they appear in T:

type $Foo = From<{ a: number }>;
type Foo = apply<$Foo, [1]>
//   Foo : { a: 1 }

type $Bar = From<[number, string]>;
type Bar = apply<$Bar, [1, 'foo']>
//   Bar : [1, "foo"]

The ordering of the arguments is only guarantied for tuples, so do supply Args when using From on multiple-fields objects.

FromDeep<⁠T, Paths>

A variation of From which enables Args to be made of paths, in order to reach deep properties:

type $Foo = From<
    { a: { b: [number, string], c: boolean } },
    [['a', 'b', 0], 'c']
>;

type Foo = apply<$Foo, [1, true]>
//   Foo : { a: { b: [1, string], c: true } }

I keep From and FromDeep separate for now because I expect the latter to perform worse. I may merge them in the future when my Typescript-foo improves.

$partial<⁠$T>

A free version of partial. Can be useful to mimic currying.

type $Adders = MapOver<[1 ,2 ,3], $partial<$Add>>
//   $Adders : [$Add<1>, $Add<2>, $Add<3>]

type Results = Ap<$Adders, [4, 5, 6]> // [5, 7, 9]
$apply<⁠Args?>

A free, flipped, version of apply which applies this[0] with Args.

type $Apply2 = $apply<[2]>;
type Result = apply<$Apply2, [$Next]> // 3

Helpers

Helpers you may use to reduce boilerplate or assist with type checking.

typedescription
Contra<⁠$T, $U>

Expect a free type with contravariant arguments.

type Foo<$T extends Contra<$T, Type<2>>> =
    apply<$T, ['foo', true]>

type Bar = Foo<$Add>
//             ~~~~

The implementation is not type safe because the constraint is widened to Type (for the implementer)

At<⁠I, this, F?>

Check the indexation of this to prevent overshooting the arity:

interface $Foo extends Type<1> { type: At<1, this> }
//                                           ----
// 'this' does not satisfy the constraint 'Type<2>'

It mainly prevents off-by-one errors or forgetting to wrap type constraints in brackets (Type<number> ≠ Type<[number]>)

Optionally take a fallback F to replace unknown:

interface $UnaryFunction extends Type<2> {
    type: (a: At<0, this, any>) => At<1, this>
}

type ReturnType = $UnaryFunction['type'] // (a: any) => unknown
A|B|C|D|E<⁠this?>

Similar to At, but also defuses type constraints with an intersection:

interface $Foo extends Type<[string]> {
    type: `${A<this>}`
}

Is equivalent to:

interface $Foo extends Type<[string]> {
    type: `${this[0] & string}`
}

When no argument is supplied, A|B|C|D|E are aliases for 0|1|2|3|4:

interface $Foo extends Type<[unknown, unknown[]]> {
    type: [A<this>, ...Checked<B, this>]
}

Is equivalent to:

interface $Foo extends Type<[unknown, unknown[]]> {
    type: [this[0], ...Checked<1, this>]
}
Checked<⁠I, this, F?>

Similar to At, but also defuses type constraints with an inline conditional:

interface $Foo extends Type<[string]> {
   type: `${Checked<0, this>}`
}

Is equivalent to:

interface $Foo extends Type<[string]> {
   type: `${this[0] extends string ? this[0] : string}`
}

F allows you to specify a fallback value, which by default is this['constraints'][N].

Optional<⁠I, this, F?>

Similar to Checked, but also deals with the possibility that the argument may not be set:

interface $Foo extends Type<[string, string?]> {
   type: `${A<this>}${Optional<1, this, ''>}`
}

Is equivalent to:

interface $Foo extends Type<[string, string?]> {
   type: `${this[0] & string}${this[1] extends undefined ? '' : this[1]}`
}

F allows you to specify a fallback value, which by default is this['constraints'][N].

Lossy<⁠I, this>

A more general version of A|B|C|D|E, working for any arity:

interface $Foo extends Type<[string]> {
    type: `${Lossy<0, this>}`
}

Is equivalent to:

interface $Foo extends Type<[string]> {
    type: `${this[0] & string}`
}
IsUnknown<⁠T>

Determine if T is strictly unknown (that is to say: excluding any and never).

unknown is the default value of every parameter and checking for it can help in situations where you need to tune the implicit return type of your free type with a surrounding conditional.

Signature<⁠$T | $T[]>

A simple util which exposes $T['constraints] and $T['type']. Useful to to quickly inspect the signature of one or multiple free types at once.

Remaining<⁠$T, N>

Alternate way to expect a free type in a type constraint by specifying the number N of arguments left to apply to $T:

interface $Foo extends Type<[number, string]> { ... }
type Foo<$T extends Remaining<$Foo, 1>> = apply<$T, ['foo']>

$T can simply be an interface:

type $I = Type<[number, string]>
type Foo<$T extends Remaining<$I, 1>> = apply<$T, ['foo']>
Widen<⁠T, $Ts?, I?>

Deeply widen a type T:

type Foo = Widen<(a: 1) => Promise<{ a: 'bar', b: [true] }>>
//   Foo : (a: number) => Promise<{ a: string, b: [boolean] }>

Works out of the box on elements of TypesMap, can otherwise be supplied a map/tuple of free types $Ts to search from.

type Wider = Widen<Foo<1>, [$Foo]> // Foo<number>

class Foo<T> { constructor (private value: T){} }
interface $Foo extends Type<1> { type: Foo<this[0]> }

Widen ignores keys of type _${string} by default, in order to preserve branded types.

type Foo<T> = { _tag: 'foo', value: T };
type Wider = Widen<Foo<5>> // { _tag: 'foo', value: number };

You can override this behaviour by providing your own pattern I.

ToTuple<⁠T>

Turn a tuple-like T, usually the result of the intersection with unknown[], into a proper tuple.

Slice<⁠T, From, To>

Extract the range [From, To[ from the tuple T. When To is omitted, slice up to the end of the tuple.

Head<⁠T> Extract the first element of the non-empty tuple T.
Tail<⁠T> Extract all but the first element of the non-empty tuple T.
Last<⁠T> Extract the last element of the tuple T or return [] if it is empty.
Init<⁠T> Extract all but the last element of the non-empty tuple T.

Application 

typedescription
apply<⁠$T, Args>

Apply a free type $T with all its arguments Args and evaluate the type.

interface $Map extends Type<2> { type: Map<A<this>, B<this>> };
type Foo = apply<$Map, [string, number]> // Map<string, number>
Generic<⁠$T>

Apply a free type $T with its type constraints.

It is mostly equivalent to $T['type'], but better handles types which have no sensible default return value:

interface $Foo extends Type<[number]> {
    type: this[0] extends number ? Foo<this[0]> : never
    // try not to do this                         -----
}

type GenericFoo = Generic<$Foo> // Foo<number>
type ReturnType = $Foo['type']; // never

I don't think it suites procedural types as much as it does "concrete" types, which is why it is used internally in unwrap, but not in Flow.

partial<⁠$T, Args, $Model?>

Return a new Type based upon $T, with Args already applied, expecting the remaining arguments.

interface $Foo extends Type<3> {
    type: Foo<A<this>, B<this>, C<this>>
}

type $Foo1 = partial<$Foo, [1]>
type Foo = apply<$Foo1, [2, 3]> // Foo<1, 2, 3>

Optionally take a $Model argument for cases where $T is a generic. It should simply repeat the type constraint:

type Foo<$Q extends Type<[number, string]>> =
    Bar<partial<$Q, [1], Type<[number, string]>>>
//                       ----------------------
type Bar<$Q extends Type<[string]>> = apply<$Q, ['bar']>
partialRight

Behaves the same as partial, but Args fills the rightmost parameters of your free type:

type $Foo23 = partialRight<$Foo, [2, 3]>
type Foo = apply<$Foo23, [1]> // Foo<1, 2, 3>
_partial<⁠$T, Args>

An experimental variant of partial, which also allows to skip arguments in the arguments list with the placeholder _.

interface $Foo extends Type<3> {
    type: Foo<A<this>, B<this>, C<this>>
}

type $Foo13 = _partial<$Foo, [1, _, 3]>
type Foo123 = apply<$Foo13, [2]> // Foo<1, 2, 3>

_partial has this bug where applying a free type with generics does not appear to produce the correct type, even though it functions correctly:

type Foo<A, B> = Bar<_partial<$Foo, [A, B]>>;
// "not a Type<1>"   ~~~~~~~~~~~~~~~~~~~~~~
type Bar<$T extends Type<1>> = apply<$T, ['bar']>
//                  -------
$Optional<⁠$T, Args>

Partially apply $T with all the elements of Args which are not never

This is useful when defining a flexible API:

interface $Box3 extends Type<[number, number, number]> {
    type: `${A<this>} x ${B<this>} x ${C<this>}`
}

type $Box<A extends number = never, B extends number = never> =
    $Optional<$Box3, [A, B]>

Free types defined this way can be partially applied with generics as well as with partial:

type Foo = apply<$Box<1>, [2, 3]>; // "1 x 2 x 3"
type Bar = apply<$Box<1, 2>, [3]>; // "1 x 2 x 3"

In some circumstances, when T is a generic, I found that a higher order type would reject a $Box<T> but would accept a partial<$Box, [T]>.

Fully applying $T with $Optional generates a Type<0> without evaluating it.

_$Optional<⁠$T, Args>

The same as $Optional, but behaves like _partial instead of partial.

type $Box<
    A extends number | _ = never,
    B extends number | _ = never,
    C extends number | _ = never
> = _$Optional<$Box3, [A, B, C]>

type Foo = apply<$Box<_, 2>, [1, 3]> // "1 x 2 x 3"
$Alter<⁠$T, Args>

The same as $Optional, with the difference that fully applying $T evaluates the type:

type Box<
    A extends number = never,
    B extends number = never,
    C extends number = never
> = $Alter<$Box3, [A, B, C]>

type Foo = Box<1, 2, 3> // "1 x 2 x 3"
type Bar = apply<Box<1, 2>, [3]> // "1 x 2 x 3"

Since free types are not first class, this behaviour may confuse users as it makes the $ prefix convention pointless. The performance is also expected to be worse than applying whatever classic type $T is based of.

Decomposition 

typedescription
unwrap<⁠T, $From?>

Decompose a type T into a pair of its constituents: { type: Type, args: unknown[] } or never if there is no match.

type Pair = unwrap<Set<number>>;
//   Pair: Unwrapped<free.Set, [number]>

Works out of the box on elements of TypesMap which is the default value of From, can otherwise be supplied a Type to unwrap from:

type A = unwrap<Foo<number>, $Foo>;
//   A: Unwrapped<$Foo, [number]>

or an object/tuple/interface containing Types to search from:

type B = unwrap<Foo<number>, [$Foo, $Bar]>;
//   B: Unwrapped<$Foo, [number]>

T is limited to arities up to 5.

unwrapDeep<⁠T, $From?>

Deeply decompose T the same way as unwrap.

type Tree = unwrapDeep< Map<1, Map<2, Map<3, 'value'>> >;

// Tree: Unwrapped<free.Map, [
//     1, Unwrapped<free.Map, [
//         2, Unwrapped<free.Map, [
//             3, "value"
//         ]>
//     ]>
// ]>
$unwrap

A free version of unwrap.

Unwrapped<⁠$T?, Args?>

A parameterised alias for the return value of unwrap.

Args is required to match $T['constraints'].

TypesMap

A global extensible map from names to Type, prepopulated by elements of the namespace free.

You can extend it with module augmentation:

declare module 'free-types' {
    interface TypesMap { Foo: $Foo }
}

class Foo<T> { constructor (private value: T) {} }
interface $Foo extends Type<1> { type:  Foo<this[0]> }
free

A namespace containing a number of built-in type constructors:

• Promise        • Array          • Set          • Map         
• Function       • ReadonlyArray  • ReadonlySet  • ReadonlyMap 
• UnaryFunction  • Tuple          • WeakSet      • WeakMap     
• Record         • ReadonlyTuple

free.Id is the identity for unary types. It leaves the input untouched. It can be used for example as a default transformation:

type Foo<$T = free.Id> = {
    foo: <A>(a: A) => apply<$T, [Bar<A>]>,
}

type SimpleFoo = Foo;
// { foo: <A>(a: A) => Bar<A> }

type FooAsync = Foo<free.Promise>
// { foo: <A>(a: A) => Promise<Bar<A>> }

type FooMap = Foo<partial<free.Map, [string]>>
// { foo: <A>(a: A) => Map<string, Bar<A>> }

free.Tuple returns its arguments list as a tuple. It can be thought of as the identity of n-ary types.

free.Function is a free version of (...args: any[]) => unknown, not Function.

free.UnaryFunction is of type Type<2>, making it easier to compose than free.Function which is of type Type<[any[], unknown]>:

type URLs = {
    A: 'https://foo'
    B: 'https://bar'
}

type Responses = {
    A: { foo: number } 
    B: { bar: number }
}

type Queries = Lift<free.UnaryFunction, [URLs, Responses]>
// {
//     A: (a: "https://foo") => { foo: number };
//     B: (a: "https://bar") => { bar: number };
// }

type Commands = MapOver<URLs, partialRight<free.UnaryFunction, [void]>>
// {
//     A: (a: "https://foo") => void;
//     B: (a: "https://bar") => void;
// }

Composition

You should read the part of the guide dedicated to composition

typedescription
$Before<⁠$T, $P, I?>

Map over the individual arguments of $T with $P before they hit $T.

type $AbsAdd = $Before<$Add, $Abs>;
type Foo = apply<$AbsAdd, [1, -2]> // same as apply<$Add, [1, 2]>

Optionally take and index I to selectively transform the argument at this position.

Pipe<⁠Args, ...$T[]>

Pipe the arguments list Args through the free types listed in ...$T[], from left to right. The composition is limited to 10 free types.

type Foo = Pipe<[1, 2], $Add, $Next, $Exclaim> // 4!

When the types don't line up, the compiler tells you precisely what went wrong, using the inferred value as a hint:

type Foo = Pipe<[1, 2], $Add, $Exclaim, $Next,>
//                                      ~~~~~
// Type '$Next' does not satisfy the constraint 'Type<["3!"], number>'.
//   Types of property 'constraints' are incompatible.
//     Type '[number]' is not assignable to type '["3!"]'.
//       Type 'number' is not assignable to type '"3!"'.

When Args contains generics, it does its best:

type Error<T extends number> = Pipe<[T], $Exclaim, $Next>;
//                                                 ~~~~~
// Type '$Next' does not satisfy the constraint
// '_<$Next, "should be", Type<[`${T & string}!` | `${T & number}!`], number>>'

Be careful when using Pipe with a generic: Typescript will eagerly try to expand the type, which can work fine when nesting types for example, but can potentially fail if resolving the generic is required to work out the validity of the composition. Another possibility is that you end up with a huge thunk of nested conditional types waiting to receive a value.

Flow<⁠$T[]>

Return the composition from left to right of the free types listed in$T[].

type $AddNextExclaim = Flow<[$Add, $Next, $Exclaim]>
type Foo = apply<[1, 2], $AddNextExclaim> // 4!
$Arg<⁠I>

Get the argument at index I

Adapters 

typedescription
$Arity<⁠N, $T>

Turn a variadic Type into a Type<N>.

$Flip<⁠$T>

Turn a Type<[A, B]> into a Type<[B, A]> and apply arguments accordingly.

$Rest<⁠$T, N?>

Turn a Type<1> expecting a Tuple of N elements into a Type<N>.

type With = apply<$Rest<$Foo>, [1, 2, 3]>;

// is equivalent to 
type Without = apply<$Foo, [[1, 2, 3]]>;

The parameter N is only required in situations where the tuple's length can't be statically known, for example in a composition using Flow.

$Spread<⁠$T>

Turn a Type<N> into a Type<1> expecting a Tuple of N elements.

type With = apply<$Spread<$Foo>, [[1, 2, 3]]>;

// is equivalent to 
type Without = apply<$Foo, [1, 2, 3]>;
$Values<⁠$T>

A looser version of $Spread which turns a Type<N> into a Type<1> expecting a Mappable. Can be useful when working with mappable-returning types.

The length and ordering of the arguments are not type checked. Supernumerary arguments are ignored.

If you supply an object type to $Values<$T>, the ordering of arguments is not guarantied. Depending on context you may want to pair it with $Constrain to disallow objects, otherwise make sure $T is commutative.

$Param<⁠I, $T>

Turn a Type<1> into a variadic Type discarding every argument except the one at index I.

type With = apply<$Param<1, $Foo>, [1, 2, 3]>;

// is equivalent to 
type Without = apply<$Foo, [2]>;
$Constrain<⁠$T, Cs>

Narrow the constraints of the free type $T using the tuple Cs and update the return type accordingly:

type $SplitDash = $Constrain<$Split<'-'>, [`${string}-${string}`]>

If Cs and $T['constraints'] are intersected with Intersect

The return type advertised in the signature is updated by applying $T with the new type constraints, which is usually not a problem, but in the case of $SplitDash it is going to give us [string, string], which is wrong because ${string} could be containing - and therefore we should have [string, string, ...string[]]

$As<⁠$T, R>

Take a free type $T and an R related to* $T['type] and return a new Type returning the intersection of R with the original return type.

R becomes the return type advertised in the signature, even when it is wider than the original.

Useful in situations where you created a free type programmatically and was thus not able to fine tune the advertised return type

Following through with the previous example:

// maybe consider writing a brand new free type though
type $SplitDash = $As<
    $Constrain<$Split<'-'>, [`${string}-${string}`]>,
    [string, string, ...string[]]
>

* that is to say one must be a subtype or a supertype of the other

$Assert<⁠T>

Not really an adapter as it does not convert a free type, but can be used as an alternative to $Constrain or $As to make a poorly typed composition type check. The advertised return type of $Assert is always never

type $Foo = Flow<[$Prop<'value'>, $Assert<number>, $Next]>
// ({ value : unknown } -> unknown), (unknown -> never), (number -> number)

The advertised return type of $Foo in the example above is unafected by the fact that the one of $Assert is never.

Higher order types 

typedescription
MapOver<⁠T, $T>

Create a new mappable of the same structure as T, populated with the results of applying the free type $T on each element of T. T must be of type $T['constraints][0].

type Foo = MapOver<[1, 2], $Next> // [2, 3]
type Bar = MapOver<(1 | 2)[], $Next> // (2 | 3)[]
type Baz = MapOver<{ a: 1, b: 2 }, $Next> // { a: 2, b: 3 }

$T can be of Type<1> or Type<2>, in which case the second argument is the key/index of the current element.

$MapOver<<⁠$T, L?>

A free version of MapOver. The optional argument L constrains it to work with tuples of that length (or arrays if L is exactly number).

Lift<⁠$T, Ts>

Similar to MapOver, but generalised for any arity.

Take a $T of type Type<N> and a tuple Ts of length N in which each element is a Mappable, and return a Mappable populated with the result of applying $T with the grandchildren of Ts grouped by key.

type FirstNames = ['Alan', 'Grace'];
type LastNames = ['Turing', 'Hopper'];

type Names = Lift<$Stitch<' '>, [FirstNames, LastNames]>;
//   Names : ["Alan Turing", "Grace Hopper"]
type FirstNames = { a: 'Alan', b: 'Grace' };
type LastNames = { a: 'Turing', b: 'Hopper' };

type Names = Lift<$Stitch<' '>, [FirstNames, LastNames]>;
//   Names : { a: "Alan Turing", b: "Grace Hopper"}

Supernumerary keys in any of the children are dropped.

$Lift<⁠$T>

A free versions of Lift

Ap<⁠$Ts, Ts>

Apply a mappable of free types $Ts with mappable of types Ts

type Foo = Ap<[$Next, $Prev], [2, 2]> // [3, 1]
type Bar = Ap<{a: $Next, b: $Prev}, {a: 2, b: 2}> // {a: 3, b: 1}

When the keys mismatch, supernumerary elements in $Ts are dropped and supernumerary elements in Ts are left unchanged.

Reduce<⁠T, $T>

Consume the mappable T by recursively applying the binary free type $T with elements of T. The return value of each iteration becomes the first argument (accumulator) of the next iteration.

type Foo = Reduce<['a', 2, true], $Stitch<':'>>; // "a:2:true"
type Bar = Reduce<{ a: 1, b: 2, c: 3, d: 4 }, $Add>; // 10
  • $T must be of type Type<[A, A], A>;
  • T must be of type [A, ...A[]] | { [k: string|number|symbol]: A }.

When T is an object type, $T has to be associative and commutative, because the ordering of values is not guaranteed. For example, Reduce<T, $Add> can safely be used with objects and tuples, but Reduce<T, $Subtract> can only be used with tuples.

$Reduce<⁠$T>

A free version of Reduce.

Since it accepts both tuples and objects, it can follow $MapOver in a composition.

I was not able to enforce that object type inputs must contain at least one element.

Fold<⁠T, I, $T>

Identical to Reduce but requires an initialiser I, allowing T to be empty.

  • $T must be of type Type<[I, A], I>
    (of course I and A can be the same);
  • T must be of type A[] | { [k: string|number|symbol]: A }.

When T is an object type, $T has to be associative and commutative, because the ordering of values is not guaranteed. For example, Fold<T, unknown, $Intersect> can safely be used with objects and tuples, but Fold<T, [], $Concat> can only be used with tuples.

Fold<⁠$T, I>

A free version of Fold.

Since it accepts both tuples and objects, it can follow $MapOver in a composition.

Match<⁠T, [P, R, K?][], M?>

Match a type T to one of the case clauses, composed of a predicate P, a response R and optionally a tuple of keys K.

Basic syntax

P can be a plain type definition

type Number = Match<2, [
    [number, 'I am a number'],
    [string, 'I am a string'],
]>; // "I am a number"

Falling through

The placeholder otherwise can be used to prevent any value from falling through:

type Nothing = Match<true, [
    [number, 'I am a number'],
    [string, 'I am a string'],
    [otherwise, 'nothing matched']
]>; // "nothing matched"

If T fall through, the return value would be a unique symbol and you would get an error:

type NotFound = Match<true, [
//                    ----
// Type 'true' does not satisfy the constraint
// '"T fell through without matching any case"
    [number, 'I am a number'],
    [string, 'I am a string'],
]>; // unique symbol

Sadly, when T is generic and you don't have an otherwise case, this error misfires. You can correct that by supplying a plain model M, which should repeat the type constraint on T:

type Switch<T extends number | string> = Match<T, [
    [number, 'I am a number'],
    [string, 'I am a string'],
], number | string>;

You can also set M to never if you want to disable the check entirely. I prefer to make it opt-out rather than opt-in.

Procedural predicates

P can be a Type<1, boolean> running arbitrary checks on T. There will be a match if the return value is true

type Answer = Match<{}, [
    [$IsEmpty, 'I am empty'],
    [otherwise, 'I am not empty'],
]>; // "I am empty"

interface $IsEmpty extends Type<1> {
    type: keyof this[0] extends never ? true : false
}

Callbacks

If R is a free type, it is applied with T.

type Answer = Match<2, [
    [number, $Next],
    [otherwise, 'NaN'],
]>; // 3

In such case, Match checks that [P] extends R['constraints'].

When P is otherwise and R is a free type, Match needs to check that [T] extends R['constraints'], which can require using a model M.

Destructuring

The input to the callback is automatically destructured as described bellow.

Tuples

The elements of a tuple are supplied to R as an arguments list:

type Answer = Match<['4', '2'],[
    [[number, number],  $Add],
    [[string string],  $Stitch<''>]
]>; // "42"
Objects

The object values are supplied to R as an arguments list:

type Three = Match<{ a: 1, b: 2 }, [
    [{ a: number, b: number },  $Add],
    [{ a: string, b: string },  Flow<[$Stitch<''>, $Length]>],
]>; // 3

Match tries to apply arguments in the order in which they appear in P but you are advised to set a specific ordering or to use a commutative callback.

Aribtrary types

Arbitrary types are destructured if P is a free type other than Type<1, boolean>, in which case their arguments list is supplied to R:

type Args<T> = Match<T, [
    [free.Map,  free.Tuple],
    [$Foo,  free.Id],
]>;

class Foo<T> { constructor(private value: T) {}}
interface $Foo extends Type<1> { type: Foo<T> }

type MapArgs = Args<Map<string, number>> // [string, number]
type FooArgs = Args<Foo<string>> // string

Filtering and reordering arguments

The tuple K specifies which arguments are passed to R and in which order.

type Three = Match<{a: 'foo', b: 2}, [
    [{a: string, b: number}, $Next, ['b']]
]>; // 3

Match checks that the keys exist in P and T, which can require supplying a model M.

Preventing destructuring

The helper Protect allows you to select which pattern you don't want to destructure:

type Protected = Match<{ a: 1 }, [
    [{ a: string }, free.Id],
    [Protect<{ a: number }>, free.Id]
    [{ a: boolean }, free.Id]
]> // { a: 1 }
$Match<⁠[P, V, K?][], M?>

A free version of Match.

Some checks like exhaustiveness become opt-in by supplying M

Filter<⁠T, F>

Filter the mappable T by the value F.

Filter<[1, 'a', 2, true], number> // [1, 2]
Filter<{ a: 1, b: 'a', c: 2, d: true }, number> // { a: 1, c: 2 }

F can also be a predicate returning either true or false

Filter<[1, 'a', 2, true], $IsNumber> // [1, 2]
Filter<{ a: 1, b: 'a', c: 2, d: true }, $IsNumber> // { a: 1, c: 2 }

interface $IsNumber extends Type<1> {
    type: this[0] extends number ? true : false
}
$Filter<⁠F>

A free version of Filter.

FilterOut<⁠T, F>

The exact opposite of Filter.

FilterOut<[1, 'a', 2, true], number> // ['a', true]
FilterOut<{ a: 1, b: 'a', c: 2, d: true }, number> // { b: 'a', d: true }

When using a predicate, it is equivalent to calling Filter with $Not<F>.

FilterOut<[1, 'a', 2, true], $IsNumber> // ['a', true]
FilterOut<{ a: 1, b: 'a', c: 2, d: true }, $IsNumber> // { b: 'a', d: true }
$FilterOut<⁠F>

A free version of FilterOut.

GroupBy<⁠T, $F>

Partition the tuple T into sub-tuples and store the result in a Struct. The name and composition of the groups is decided by calling the predicate $P of type Type<[member: unknown], PropertyKey> on each element of T.

type NumericList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
type Numbers = NumericList[number];

interface $Sizes extends Type<[Numbers], string> {
    type: this[0] extends 0 ? 'null'
        : this[0] extends 1|2|3 ? 'small'
        : this[0] extends 4|5|6 ? 'medium'
        : 'big'
}

type GroupedBySize = GroupBy<NumericList, $Sizes>;
// {
//     null: [0],
//     small: [1, 2, 3],
//     medium: [4, 5, 6],
//     big: [7, 8, 9]
// }
$GroupBy<⁠$P>

A free version of GroupBy

GroupUnionBy<⁠T, $P, $T>

Partition the union U into sub-unions and store the result in a Struct. The name and composition of the groups is decided by calling the predicate $P of type Type<[member: unknown], PropertyKey> on each member of U.

type Numbers = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

interface $Sizes extends Type<[Numbers], string> {
    type: this[0] extends 0 ? 'null'
        : this[0] extends 1|2|3 ? 'small'
        : this[0] extends 4|5|6 ? 'medium'
        : 'big'
}

type GroupedBySize = GroupByUnion<Numbers, $Sizes>;
// {
//     null: 0,
//     small: 1 | 2 | 3,
//     medium: 4 | 5 | 6,
//     big: 7 | 8 | 9
// }

Optionally take a transfrom $T of type Type<[union: unknown, length?: number]> to map over the sub-unions as they are inserted. $T exposes the length of the sub-union as a second optional parameter.

type GroupedBySize = GroupBy<Numbers, $Sizes, $Addlen>;
// {
//     null: 1,
//     small: 4 | 5 | 6,
//     medium: 7 | 8 | 9,
//     big: 10 | 11 | 12
// }

interface $AddLen extends $Transform {
    type: Add<this[0], this[1]>
}
$GroupUnionBy<⁠$P, $T>

A free version of GroupUnionBy

Operators and other utils 

Every n-ary type listed bellow can be partially applied with generics. For convenience, this type of application behaves like PartialRight on non-commutative binary types such as $Exclude $Subtract or $Concat.

Set operations

typedescription
$Intersect A free version of A & B with a slight modification in that the intersection is simplified to A or B if one is a subtype of the other.
$Unionize A free version of A | B
$Exclude A free version of Exclude<⁠A, B>

Boolean logic

false | never | any are considered falsy values

typedescription
$Extends A free version of A extends B, returns boolean
$Includes A free version of B extends A, returns boolean code>
$RelatesTo A free version of A extends B OR B extends A, returns boolean
$Eq A free version of A extends B AND B extends A, returns boolean
$Not<⁠$T> An adapter inverting a Type<⁠1, boolean>
$And A free version of A && B
$Or A free version of A || B
$Fork<⁠$P, $A, $B, C>

Take a predicate $P of type Type<unknown[], boolean>, a left branch $A and a right branch $B and return a new Type which dispatches to $A or $B depending on the result of applying $P with the same arguments.

By default the constraints on the new Type are the union of the contraints of $P, $A and $B but you can override them with C.

$Choose<⁠$P>

Take a predicate $P of type Type<2, boolean> and return a new Type<2> with the same constraints which returns its first or second argument depending on the result of applying those arguments to itself.

Tuples

typedescription
$Head A free version of T[0]
$Tail A free version of [unknown, ...infer T]
$Last A free version of T[T['length'] - 1]
$Init A free version of [...infer T, unknown]
$Concat A free version of [...A, ...B], [...A, B], [A, ...B], [A, B]

Strings

By Showable I mean string | number | bigint | boolean | null | undefined

typedescription
Show<⁠T, Depth?>

Recursively convert an arbitrary type to a string. The Depth limit is 10 by default.

Depends on TypesMap. The key in the map is used as the string representation.

$Show<⁠Depth?> A free version of Show
$Length Get the length of a string
$ParseInt

convert a `${number}` to a number.

Limited to the range [0, 64] for compatibility.

Since TS 4.8 you can implement an unbounded version with:

T extends `${infer N extends number}` ? N : number
$Stitch A free version of ${B}${A}${C} where A, B and C are Showable
$Join<⁠S> Intersperse a tuple of Showable with the supplied separator and merge the result into one string. If the tuple is empty, $Join returns ""
$Split Split the string B into a tuple of substrings with the separator A
$StrReplace Recursively replace A with B in C

Mappables

typedescription
$Prop<⁠K?, R?>

Index a Mappable

type Foo = apply<$Prop<'a'>, [{ a: 1, b: 2 }]>; // 1
type Foo = apply<$Prop, ['a', { a: 1, b: 2 }]>; // 1

Partially applying $Prop with generics enables dependent constraints:

// $GetA : { [k: string | number | symbol]: unknown } -> unknown
type $GetA = partial<$Prop, ['a']>;

// $GetA : { a: unknown } -> unknown
type $GetA = $Prop<'a'>;

// $GetA : { a: number } -> number
type $GetA = $Prop<'a', number>;

Because R is optional, it can only be set with Generics

$SetProp<⁠V?, K?>

Set the property matching the key K to the value V in the input object type.

type Foo = apply<$SetProp<42>, ['a', {a: 1}]>; // { a: 42 }
type Foo = apply<$SetProp<42, 'a'>, [{a: 1}]>; // { a: 42 }
type Foo = apply<$SetProp, [42, 'a', {a: 1}]>; // { a: 42 }

If K does not exist, the input is returned unaltered.

$SetProp does not accept tuples

$Index<⁠I, R?>

A stricter version of $Prop which only accepts arrays/tuples

$SetIndex<⁠V?, I?>

Similar to $SetProp with tuples:

type Foo = apply<$SetIndex, [42, 0, [1]]>; // [42]
type Foo = apply<$SetIndex<42>, [0, [1]]>; // [42]
type Foo = apply<$SetIndex<42, 0>, [[1]]>; // [42]

Arithmetic

typedescription
$Next A free version of T + 1, with T in the range [0, 63].
$Prev A free version of T - 1, with T in the range [1, 64].
$Abs A free version of |T| with T in the range [-64, Infinity[.
$Add* A free version of A + B
$Multiply* A free version of A * B
$Subtract* A free version of A - B
$Divide* A free version of A / B (Euclidean)
$Lt* A free version ofA < B
$Lte* A free version ofA <= B
$Gt* A free version ofA > B
$Gte* A free version ofA >= B
$Max* A free version of Max<⁠A, B>
$Min* A free version of Min<⁠A, B>

* the inputs and output of these types must be in the range [0, 64]