Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/thomasaribart/ts-algebra
Types on steroids 💊
https://github.com/thomasaribart/ts-algebra
Last synced: 13 days ago
JSON representation
Types on steroids 💊
- Host: GitHub
- URL: https://github.com/thomasaribart/ts-algebra
- Owner: ThomasAribart
- License: mit
- Created: 2021-08-20T16:57:55.000Z (about 3 years ago)
- Default Branch: main
- Last Pushed: 2024-09-02T15:19:46.000Z (2 months ago)
- Last Synced: 2024-10-18T11:37:37.409Z (26 days ago)
- Language: TypeScript
- Homepage:
- Size: 1.15 MB
- Stars: 18
- Watchers: 3
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
💖 _Huge thanks to the [sponsors](https://github.com/sponsors/ThomasAribart) who help me maintain this repo:_
# 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 tonever
)---
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 ObjectBtype 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 itemtype 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` |