@stephanealnet-signalwire/ts-algebra v1.1.0
Types on steroids 💊
ts-algebra
exposes a subset of TS types called Meta-types: Meta-types are types that encapsulate other types.
import { Meta } from "ts-algebra";
type MetaString = Meta.Primitive<string>;
The encapsulated type can be retrieved using the Resolve
operation.
type Resolved = Meta.Resolve<MetaString>;
// => string 🙌
You can also use the more compact M
notation:
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Primitive<string>
>;
Okay, but... why ? 🤔
Meta-types allow operations that are not possible with conventional types.
For instance, they allow new "intersect" and "exclude" operations, and handling objects additional properties:
type MyObject = {
str: string; // <= ❌ "str" is assignable to string
[key: string]: number;
};
type MyObjectKeys = keyof MyObject;
// => string <= ❌ Unable to isolate "str"
Think of meta-types as a parallel universe where all kinds of magic can happen 🌈 Once your computations are over, you can retrieve the results by resolving them.
Meta-types were originally part of json-schema-to-ts. Check it to see a real-life usage.
Table of content
☁️ Installation
# npm
npm install --save-dev ts-algebra
# yarn
yarn add --dev ts-algebra
🧮 Cardinality
A bit of theory first:
- The cardinality of a type is the number of distinct values (potentially infinite) that can be assigned to it
- A meta-type is said representable if at least one value can be assigned to its resolved type (cardinality ≥ 1)
An important notion to keep in mind using ts-algebra
:
Any other non-representable meta-type (e.g. an object with a non-representable but required property) will be instanciated as M.Never
.
There are drawbacks to this choice (the said property is hard to find and debug) but stronger benefits: This drastically reduces type computations, in particular in intersections and exclusions. This is crucial for performances and stability.
✨ Meta-types
Any
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Any
>;
// => unknown
Never
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Never
>;
// => never
Const
Used for types with cardinalities of 1.
Arguments:
- Value (type)
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Const<"I love pizza">
>;
// => "I love pizza"
Enum
Used for types with finite cardinalities.
Arguments:
- Values (type union)
import { M } from "ts-algebra";
type Food = M.Resolve<
M.Enum<"pizza" | "tacos" | "fries">
>;
// => "pizza" | "tacos" | "fries"
☝️
M.Enum<never>
is non-representable
Primitive
Used for either string
, number
, boolean
or null
.
Arguments:
- Value (string | number | boolean | null)
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Primitive<string>
>;
// => string
Array
Used for lists of items of the same type.
Arguments:
- Items (?meta-type = M.Any)
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Array
>;
// => unknown[]
type Resolved = M.Resolve<
M.Array<M.Primitive<string>>
>;
// => string[]
☝️ Any meta-array is representable by
[]
Tuple
Used for finite, ordered lists of items of different types.
Meta-tuples can have additional items, typed as M.Never
by default. Thus, any meta-tuple is considered closed (additional items not allowed), unless a representable additional items meta-type is specified, in which case it becomes open.
Arguments:
- RequiredItems (meta-type[]):
- AdditionalItems (?meta-type = M.Never): Type of additional items
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Tuple<[M.Primitive<string>]>
>;
// => [string]
type Resolved = M.Resolve<
M.Tuple<
[M.Primitive<string>],
M.Primitive<string>
>
>;
// => [string, ...string[]]
☝️ A meta-tuple is non-representable if one of its required items is non-representable
Object
Used for sets of key-value pairs (properties) which can be required or not.
Meta-objects can have additional properties, typed as M.Never
by default. Thus, any meta-object is considered closed (additional properties not allowed), unless a representable additional properties meta-type is specified, in which case it becomes open.
In presence of named properties, open meta-objects additional properties are resolved as unknown
to avoid conflicts. However, they are used as long as the meta-type is not resolved (especially in intersections and exclusions).
Arguments:
- NamedProperties (?{ key:string: meta-type } = {})
- RequiredPropertiesKeys (?string union = never)
- AdditionalProperties (?meta-type = M.Never): The type of additional properties
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Object<
{
required: M.Primitive<string>;
notRequired: M.Primitive<null>;
},
"required",
M.Primitive<number>
>
>;
// => {
// req: string,
// notRequired?: null,
// [key: string]: unknown
// }
☝️ A meta-object is non-representable if one of its required properties value is non-representable:
- If it is a non-representable named property
- If it is an additional property, and the object is closed
Union
Used to combine meta-types in a union of meta-types.
Arguments:
- Values (meta-type union)
import { M } from "ts-algebra";
type Food = M.Resolve<
M.Union<
| M.Primitive<number>
| M.Enum<"pizza" | "tacos" | "fries">
| M.Const<true>
>
>;
// => number
// | "pizza" | "tacos" | "fries"
// | true
☝️ A meta-union is non-representable if it is empty, or if none of its elements is representable
🔧 Methods
Resolve
Resolves the meta-type to its encapsulated type.
Arguments:
- MetaType (meta-type)
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Primitive<string>
>;
// => string
Intersect
Takes two meta-types as arguments, and returns their intersection as a meta-type.
Arguments:
- LeftMetaType (meta-type)
- RightMetaType (meta-type)
import { M } from "ts-algebra";
type Intersected = M.Intersect<
M.Primitive<string>,
M.Enum<"I love pizza"
| ["tacos"]
| { and: "fries" }
>
>
// => M.Enum<"I love pizza">
Meta-type intersections differ from conventional intersections:
type ConventionalIntersection =
{ str: string } & { num: number };
// => { str: string, num: number }
type MetaIntersection = M.Intersect<
M.Object<
{ str: M.Primitive<string> },
"str"
>,
M.Object<
{ num: M.Primitive<number> },
"num"
>
>;
// => M.Never: "num" is required in B
// ...but denied in A
Intersections are recursively propagated among tuple items and object properties, and take into account additional items and properties:
type Intersected = M.Intersect<
M.Tuple<
[M.Primitive<number>],
M.Primitive<string>
>,
M.Tuple<
[M.Enum<"pizza" | 42>],
M.Enum<"fries" | true>
>
>;
// => M.Tuple<
// [M.Enum<42>],
// M.Enum<"fries">
// >
type Intersected = M.Intersect<
M.Object<
{ food: M.Primitive<string> },
"food",
M.Any
>,
M.Object<
{ age: M.Primitive<number> },
"age",
M.Enum<"pizza" | "fries" | 42>
>
>;
// => M.Object<
// {
// food: M.Enum<"pizza" | "fries">,
// age: M.Primitive<number>
// },
// "food" | "age",
// M.Enum<"pizza" | "fries" | 42>
// >
Intersections are distributed among unions:
type Intersected = M.Intersect<
M.Primitive<string>,
M.Union<
| M.Const<"pizza">
| M.Const<42>
>
>;
// => M.Union<
// | M.Const<"pizza">
// | M.Never
// >
Exclude
Takes two meta-types as arguments, and returns their exclusion as a meta-type.
Arguments:
- SourceMetaType (meta-type)
- ExcludedMetaType (meta-type)
import { M } from "ts-algebra";
type Excluded = M.Exclude<
M.Enum<"I love pizza"
| ["tacos"]
| { and: "fries" }
>,
M.Primitive<string>,
>
// => M.Enum<
// | ["tacos"]
// | { and: "fries" }
// >
Meta-type exclusions differ from conventional exclusions:
type ConventionalExclusion = Exclude<
{ req: string; notReq?: string },
{ req: string }
>;
// => never
// ObjectA is assignable to ObjectB
type MetaExclusion = M.Exclude<
M.Object<
{
req: M.Primitive<string>;
notReq: M.Primitive<string>;
},
"req"
>,
M.Object<
{ req: M.Primitive<string> },
"req"
>
>;
// => ObjectA
// Exclusion is still representable
type ConventionalExclusion = Exclude<
{ food: "pizza" | 42 },
{ [k: string]: number }
>;
// => { food: "pizza" | 42 }
type MetaExclusion = M.Exclude<
M.Object<
{ food: M.Enum<"pizza" | 42> },
"food"
>,
M.Object<
{},
never,
M.Primitive<number>
>
>;
// => M.Object<
// { food: M.Enum<"pizza"> },
// "food"
// >
When exclusions can be collapsed on a single item or property, they are recursively propagated among tuple items and object properties, taking into account additional items and properties:
type Excluded = M.Exclude<
M.Tuple<[M.Enum<"pizza" | 42>]>,
M.Tuple<[M.Primitive<number>]>
>;
// => M.Tuple<[M.Enum<"pizza">]>
type Excluded = M.Exclude<
M.Tuple<
[M.Enum<"pizza" | 42>],
M.Enum<"fries" | true>
>,
M.Tuple<
[M.Primitive<number>],
M.Primitive<string>
>
>;
// => TupleA
// Exclusion is not collapsable on a single item
type Excluded = M.Exclude<
M.Object<
{
reqA: M.Enum<"pizza" | 42>;
reqB: M.Enum<"pizza" | 42>;
},
"reqA" | "reqB"
>,
M.Object<
{},
never,
M.Primitive<number>
>
>;
// => ObjectA
// Exclusion is not collapsable on a single property
Exclusions are distributed among unions:
type Excluded = M.Exclude<
M.Union<
| M.Const<"pizza">
| M.Const<42>
>,
M.Primitive<number>
>;
// => M.Union<
// | M.Const<"pizza">
// | M.Never
// >
Exluding a union returns the intersection of the exclusions of all elements, applied separately:
type Excluded = M.Exclude<
M.Enum<42 | "pizza" | true>,
M.Union<
| M.Primitive<number>
| M.Primitive<boolean>
>
>;
// => M.Enum<"pizza">
🚧 Type constraints
To prevent errors, meta-types inputs are validated against type constraints:
type Invalid = M.Array<
string // <= ❌ Meta-type expected
>;
If you need to use them, all type constraints are also exported:
Meta-type | Type constraint |
---|---|
M.Any | M.AnyType = M.Any |
M.Never | M.NeverType = M.Never |
M.Const | M.ConstType = M.Const<any> |
M.Enum | M.EnumType = M.Enum<any> |
M.Primitive | M.PrimitiveType = M.Primitive<null \| boolean \| number \| string> |
M.Array | M.ArrayType = M.Array<M.Type> |
M.Tuple | M.TupleType = M.Tuple<M.Type[], M.Type> |
M.Object | M.ObjectType = M.Object<Record<string, M.Type>, string, M.Type> |
M.Union | M.UnionType = M.Union<M.Type> |
- | M.Type = Union of the above |
✂️ Unsafe types and methods
In deep and self-referencing computations like in json-schema-to-ts, type constraints can become an issue, as the compiler may not be able to confirm the input type validity ahead of usage.
type MyArray = M.Array<
VeryDeepTypeComputation<
...
> // <= 💥 Type constraint can break
>
For such cases, ts-algebra
exposes "unsafe" types and methods, that behave the same as "safe" ones but removing any type constraints. If you use them, beware: The integrity of the compiling is up to you 😉
Safe | Unsafe |
---|---|
M.Any | - |
M.Never | - |
M.Const | - |
M.Enum | - |
M.Primitive | M.$Primitive |
M.Array | M.$Array |
M.Tuple | M.$Tuple |
M.Object | M.$Object |
M.Union | M.$Union |
M.Resolve | M.$Resolve |
M.Intersect | M.$Intersect |
M.Exclude | M.$Exclude |
2 years ago
2 years ago