If you use this repo, star it ✨

# Types on steroids 💊 `ts-algebra` exposes a subset of TS types called **Meta-types**: Meta-types are types that encapsulate other types. ```typescript import { Meta } from "ts-algebra"; type MetaString = Meta.Primitive; ``` The encapsulated type can be retrieved using the `Resolve` operation. ```typescript type Resolved = Meta.Resolve; // => string 🙌 ``` You can also use the more compact `M` notation: ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Primitive >; ``` ## Okay, but... why ? 🤔 Meta-types allow operations that **are not possible with conventional types**. For instance, they allow new ["intersect"](#intersect) and ["exclude"](#exclude) operations, and handling objects additional properties: ```typescript 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](https://github.com/ThomasAribart/json-schema-to-ts). Check it to see a real-life usage. ## Table of content - [Installation](#%EF%B8%8F-installation) - [Cardinality](#-cardinality) - [Meta-types](#-meta-types) - [Never](#never) - [Any](#any) - [Const](#const) - [Enum](#enum) - [Primitive](#primitive) - [Array](#array) - [Tuple](#tuple) - [Object](#object) - [Union](#union) - [Methods](#-methods) - [Resolve](#resolve) - [Intersect](#intersect) - [Exclude](#exclude) - [Deserialization](#-deserialization) - [Type constraints](#-type-constraints) - [Unsafe types](#%EF%B8%8F-unsafe-types-and-methods) ## ☁️ Installation ```bash # npm npm install --save-dev ts-algebra # yarn yarn add --dev ts-algebra ``` ## 🧮 Cardinality A bit of theory first: - The [**cardinality**](https://en.wikipedia.org/wiki/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`: ---

M.Never is the only Meta-Type that is non-representable
(i.e. that resolves to never)

--- 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](#intersect) and [exclusions](#exclude). This is crucial for performances and stability. ## ✨ Meta-types ### Never ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Never >; // => never ``` ### Any **Arguments:** - IsSerialized (?boolean = false): See [deserialization](#-deserialization) - Deserialized (?type = never): See [deserialization](#-deserialization) ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Any >; // => unknown ``` ### Const Used for types with [cardinalities](#meta-types) of 1. **Arguments:** - Value (type) - IsSerialized (?boolean = false): See [deserialization](#-deserialization) - Deserialized (?type = never): See [deserialization](#-deserialization) ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Const<"I love pizza"> >; // => "I love pizza" ``` ### Enum Used for types with finite [cardinalities](#meta-types). **Arguments:** - Values (type union) - IsSerialized (?boolean = false): See [deserialization](#-deserialization) - Deserialized (?type = never): See [deserialization](#-deserialization) ```typescript import { M } from "ts-algebra"; type Food = M.Resolve< M.Enum<"pizza" | "tacos" | "fries"> >; // => "pizza" | "tacos" | "fries" ``` > ☝️ `M.Enum` is [non-representable](#✨-meta-types) ### Primitive Used for either `string`, `number`, `boolean` or `null`. **Arguments:** - Value (string | number | boolean | null) - IsSerialized (?boolean = false): See [deserialization](#-deserialization) - Deserialized (?type = never): See [deserialization](#-deserialization) ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Primitive >; // => string ``` ### Array Used for lists of items of the same type. **Arguments:** - Items (?meta-type = M.Any) - IsSerialized (?boolean = false): See [deserialization](#-deserialization) - Deserialized (?type = never): See [deserialization](#-deserialization) ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Array >; // => unknown[] type Resolved = M.Resolve< M.Array> >; // => 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`](#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 - IsSerialized (?boolean = false): See [deserialization](#-deserialization) - Deserialized (?type = never): See [deserialization](#-deserialization) ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Tuple<[M.Primitive]> >; // => [string] type Resolved = M.Resolve< M.Tuple< [M.Primitive], M.Primitive > >; // => [string, ...string[]] ``` > ☝️ A meta-tuple is [non-representable](#✨-meta-types) 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`](#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](#intersect) and [exclusions](#exclude)). **Arguments:** - NamedProperties (?{ [key:string]: meta-type } = {}) - RequiredPropertiesKeys (?string union = never) - AdditionalProperties (?meta-type = M.Never): The type of additional properties - CloseOnResolve (?boolean = false): Ignore `AdditionalProperties` at resolution time - IsSerialized (?boolean = false): See [deserialization](#-deserialization) - Deserialized (?type = never): See [deserialization](#-deserialization) ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Object< { required: M.Primitive; notRequired: M.Primitive; }, "required", M.Primitive > >; // => { // req: string, // notRequired?: null, // [key: string]: unknown // } type ClosedOnResolve = M.Resolve< M.Object< { required: M.Primitive; notRequired: M.Primitive; }, "required", M.Primitive, false > >; // => { // req: string, // notRequired?: null, // } ``` > ☝️ A meta-object is [non-representable](#✨-meta-types) 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) ```typescript import { M } from "ts-algebra"; type Food = M.Resolve< M.Union< | M.Primitive | M.Enum<"pizza" | "tacos" | "fries"> | M.Const > >; // => number // | "pizza" | "tacos" | "fries" // | true ``` > ☝️ A meta-union is [non-representable](#✨-meta-types) if it is empty, or if none of its elements is representable > ☝️ Along with [M.Never](#never), M.Union is the only meta-type that doesn't support [serialization](#-deserialization) ## 🔧 Methods ### Resolve Resolves the meta-type to its encapsulated type. **Arguments:** - MetaType (meta-type) ```typescript import { M } from "ts-algebra"; type Resolved = M.Resolve< M.Primitive >; // => string ``` ### Intersect Takes two meta-types as arguments, and returns their intersection as a meta-type. **Arguments:** - LeftMetaType (meta-type) - RightMetaType (meta-type) ```typescript import { M } from "ts-algebra"; type Intersected = M.Intersect< M.Primitive, M.Enum<"I love pizza" | ["tacos"] | { and: "fries" } > > // => M.Enum<"I love pizza"> ``` Meta-type intersections differ from conventional intersections: ```typescript type ConventionalIntersection = { str: string } & { num: number }; // => { str: string, num: number } type MetaIntersection = M.Intersect< M.Object< { str: M.Primitive }, "str" >, M.Object< { num: M.Primitive }, "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: ```typescript type Intersected = M.Intersect< M.Tuple< [M.Primitive], M.Primitive >, 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 }, "food", M.Any >, M.Object< { age: M.Primitive }, "age", M.Enum<"pizza" | "fries" | 42> > >; // => M.Object< // { // food: M.Enum<"pizza" | "fries">, // age: M.Primitive // }, // "food" | "age", // M.Enum<"pizza" | "fries" | 42> // > ``` Intersections are distributed among unions: ```typescript type Intersected = M.Intersect< M.Primitive, 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) ```typescript import { M } from "ts-algebra"; type Excluded = M.Exclude< M.Enum<"I love pizza" | ["tacos"] | { and: "fries" } >, M.Primitive, > // => M.Enum< // | ["tacos"] // | { and: "fries" } // > ``` Meta-type exclusions differ from conventional exclusions: ```typescript type ConventionalExclusion = Exclude< { req: string; notReq?: string }, { req: string } >; // => never // ObjectA is assignable to ObjectB type MetaExclusion = M.Exclude< M.Object< { req: M.Primitive; notReq: M.Primitive; }, "req" >, M.Object< { req: M.Primitive }, "req" > >; // => ObjectA // Exclusion is still representable ``` ```typescript 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 > >; // => 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: ```typescript type Excluded = M.Exclude< M.Tuple<[M.Enum<"pizza" | 42>]>, M.Tuple<[M.Primitive]> >; // => M.Tuple<[M.Enum<"pizza">]> type Excluded = M.Exclude< M.Tuple< [M.Enum<"pizza" | 42>], M.Enum<"fries" | true> >, M.Tuple< [M.Primitive], M.Primitive > >; // => 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 > >; // => ObjectA // Exclusion is not collapsable on a single property ``` Exclusions are distributed among unions: ```typescript type Excluded = M.Exclude< M.Union< | M.Const<"pizza"> | M.Const<42> >, M.Primitive >; // => M.Union< // | M.Const<"pizza"> // | M.Never // > ``` Excluding a union returns the intersection of the exclusions of all elements, applied separately: ```typescript type Excluded = M.Exclude< M.Enum<42 | "pizza" | true>, M.Union< | M.Primitive | M.Primitive > >; // => M.Enum<"pizza"> ``` ## 📦 Deserialization All meta-types except [`M.Never`](#never) and [`M.Union`](#union) can carry an extra type for [deserialization](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html) purposes. This extra-type will be passed along in operations and override the resolved type. For instance, it is common to deserialize timestamps as `Date` objects. The last two arguments of [`M.Primitive`](#primitive) can be used to implement this: ```typescript type MetaTimestamp = M.Primitive< string, true, // <= enables deserialization (false by default) Date // <= overrides resolved type >; type Resolved = M.Resolve; // => Date ``` Note that `MetaTimestamp` will still be considered as a string meta-type until it is resolved: Deserialization only take effect **at resolution time**. ```typescript type Intersected = M.Intersect< MetaTimestamp, M.Object<{}, never, M.Any> // <= Date is an object... >; // => M.Never // ...but doesn't intersect Timestamp ``` In representable [intersections](#intersect): - If no meta-type is serialized, the resulting intersection is not serialized. - If only one meta-type (left or right) is serialized, the resulting intersection inherits from its deserialization properties. - If both left and right meta-types are serialized, the resulting intersection inherits from both deserialization properties, through a conventional intersection (`A & B`). ```typescript type MetaBrandedString = M.Primitive< string, true, { brand: "timestamp" } >; type Resolved = M.Resolve< M.Intersect< MetaTimestamp, MetaBrandedString > > // => Date & { brand: "timestamp" } ``` In representable [exclusions](#exclude): - If the source meta-type is not serialized, the resulting exclusion is not serialized. - If the source meta-type is serialized, the resulting exclusion inherits of its deserialization properties. ## 🚧 Type constraints To prevent errors, meta-types inputs are validated against type constraints: ```typescript 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` | | `M.Enum` | `M.EnumType` = `M.Enum` | | `M.Primitive` | `M.PrimitiveType` = `M.Primitive` | | `M.Array` | `M.ArrayType` = `M.Array` | | `M.Tuple` | `M.TupleType` = `M.Tuple` | | `M.Object` | `M.ObjectType` = `M.Object, string, M.Type>` | | `M.Union` | `M.UnionType` = `M.Union` | | - | `M.Type` = Union of the above | ## ✂️ Unsafe types and methods In deep and self-referencing computations like in [json-schema-to-ts](https://github.com/ThomasAribart/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. ```typescript 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` |