npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

Iโ€™ve always been into building performant and accessible sites, but lately Iโ€™ve been taking it extremely seriously. So much so that Iโ€™ve been building a tool to help me optimize and monitor the sites that I build to make sure that Iโ€™m making an attempt to offer the best experience to those who visit them. If youโ€™re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, ๐Ÿ‘‹, Iโ€™m Ryan Hefnerย  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If youโ€™re interested in other things Iโ€™m working on, follow me on Twitter or check out the open source projects Iโ€™ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soonโ€“ish.

Open Software & Tools

This site wouldnโ€™t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you ๐Ÿ™

ยฉ 2024 โ€“ย Pkg Stats / Ryan Hefner

ts-algebra

v2.0.0

Published

Types on steroids ๐Ÿ’Š

Downloads

2,328,181

Readme

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

Never

import { M } from "ts-algebra";

type Resolved = M.Resolve<
  M.Never
>;
// => never

Any

Arguments:

import { M } from "ts-algebra";

type Resolved = M.Resolve<
  M.Any
>;
// => unknown

Const

Used for types with cardinalities of 1.

Arguments:

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:

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:

import { M } from "ts-algebra";

type Resolved = M.Resolve<
  M.Primitive<string>
>;
// => string

Array

Used for lists of items of the same type.

Arguments:

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
  • IsSerialized (?boolean = false): See deserialization
  • Deserialized (?type = never): See deserialization
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
  • CloseOnResolve (?boolean = false): Ignore AdditionalProperties at resolution time
  • IsSerialized (?boolean = false): See deserialization
  • Deserialized (?type = never): See deserialization
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
// }

type ClosedOnResolve = M.Resolve<
  M.Object<
    {
      required: M.Primitive<string>;
      notRequired: M.Primitive<null>;
    },
    "required",
    M.Primitive<number>,
    false
  >
>;
// => {
//  req: string,
//  notRequired?: null,
// }

โ˜๏ธ 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

โ˜๏ธ Along with M.Never, M.Union is the only meta-type that doesn't support serialization

๐Ÿ”ง 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
// >

Excluding 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">

๐Ÿ“ฆ Deserialization

All meta-types except M.Never and M.Union can carry an extra type for deserialization 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 can be used to implement this:

type MetaTimestamp = M.Primitive<
  string,
  true, // <= enables deserialization (false by default)
  Date // <= overrides resolved type
>;

type Resolved = M.Resolve<MetaTimestamp>;
// => Date

Note that MetaTimestamp will still be considered as a string meta-type until it is resolved: Deserialization only take effect at resolution time.

type Intersected = M.Intersect<
  MetaTimestamp,
  M.Object<{}, never, M.Any> // <= Date is an object...
>;
// => M.Never
// ...but doesn't intersect Timestamp

In representable intersections:

  • 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).
type MetaBrandedString = M.Primitive<
  string,
  true,
  { brand: "timestamp" }
>;

type Resolved = M.Resolve<
  M.Intersect<
    MetaTimestamp,
    MetaBrandedString
  >
>
// => Date & { brand: "timestamp" }

In representable exclusions:

  • 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:

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 |