Security News
pnpm 10.0.0 Blocks Lifecycle Scripts by Default
pnpm 10 blocks lifecycle scripts by default to improve security, addressing supply chain attack risks but sparking debate over compatibility and workflow changes.
ts-algebra
Advanced tools
The ts-algebra npm package provides utilities for advanced type-level programming in TypeScript. It allows developers to perform complex type manipulations and algebraic operations on types, enabling more expressive and safer TypeScript code.
Type-Level Arithmetic
This feature allows you to perform arithmetic operations at the type level. In this example, the `Add` utility is used to add two numbers, resulting in a type that represents the sum.
import { Add } from 'ts-algebra';
type Result = Add<3, 4>; // Result is 7
Type-Level Logic
This feature provides logical operations at the type level. The `And` and `Or` utilities can be used to combine boolean types, resulting in a type that represents the logical outcome.
import { And, Or } from 'ts-algebra';
type True = true;
type False = false;
type Result1 = And<True, False>; // Result1 is false
type Result2 = Or<True, False>; // Result2 is true
Type-Level Comparisons
This feature allows you to perform comparisons at the type level. The `GreaterThan` utility checks if one number type is greater than another, resulting in a boolean type.
import { GreaterThan } from 'ts-algebra';
type Result = GreaterThan<5, 3>; // Result is true
Type-fest is a collection of essential TypeScript types. It provides a wide range of utility types for various use cases, including type transformations and type-level logic. Compared to ts-algebra, type-fest offers a broader set of utilities but may not be as focused on algebraic operations.
Utility-types is a package that provides a set of utility types for TypeScript. It includes types for deep partials, readonly, and other common transformations. While it offers useful utilities for type manipulation, it does not focus specifically on algebraic operations like ts-algebra.
Ts-toolbelt is a comprehensive collection of type utilities for TypeScript. It includes advanced type-level programming tools, such as type-level arithmetic, logic, and comparisons, similar to ts-algebra. However, ts-toolbelt offers a more extensive set of utilities and is designed to be a complete toolkit for type-level programming.
If you use this repo, star it ✨
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>
>;
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.
# npm
npm install --save-dev ts-algebra
# yarn
yarn add --dev ts-algebra
A bit of theory first:
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 and exclusions. This is crucial for performances and stability.
import { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Never
>;
// => never
Arguments:
IsSerialized (?boolean = false)
: See deserializationDeserialized (?type = never)
: See deserializationimport { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Any
>;
// => unknown
Used for types with cardinalities of 1.
Arguments:
Value (type)
IsSerialized (?boolean = false)
: See deserializationDeserialized (?type = never)
: See deserializationimport { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Const<"I love pizza">
>;
// => "I love pizza"
Used for types with finite cardinalities.
Arguments:
Values (type union)
IsSerialized (?boolean = false)
: See deserializationDeserialized (?type = never)
: See deserializationimport { M } from "ts-algebra";
type Food = M.Resolve<
M.Enum<"pizza" | "tacos" | "fries">
>;
// => "pizza" | "tacos" | "fries"
☝️
M.Enum<never>
is non-representable
Used for either string
, number
, boolean
or null
.
Arguments:
Value (string | number | boolean | null)
IsSerialized (?boolean = false)
: See deserializationDeserialized (?type = never)
: See deserializationimport { M } from "ts-algebra";
type Resolved = M.Resolve<
M.Primitive<string>
>;
// => string
Used for lists of items of the same type.
Arguments:
Items (?meta-type = M.Any)
IsSerialized (?boolean = false)
: See deserializationDeserialized (?type = never)
: See deserializationimport { 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
[]
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 itemsIsSerialized (?boolean = false)
: See deserializationDeserialized (?type = never)
: See deserializationimport { 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
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 propertiesCloseOnResolve (?boolean = false)
: Ignore AdditionalProperties
at resolution timeIsSerialized (?boolean = false)
: See deserializationDeserialized (?type = never)
: See deserializationimport { 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
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
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
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
// >
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">
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:
A & B
).type MetaBrandedString = M.Primitive<
string,
true,
{ brand: "timestamp" }
>;
type Resolved = M.Resolve<
M.Intersect<
MetaTimestamp,
MetaBrandedString
>
>
// => Date & { brand: "timestamp" }
In representable exclusions:
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 |
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 |
FAQs
Types on steroids 💊
The npm package ts-algebra receives a total of 394,666 weekly downloads. As such, ts-algebra popularity was classified as popular.
We found that ts-algebra demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
pnpm 10 blocks lifecycle scripts by default to improve security, addressing supply chain attack risks but sparking debate over compatibility and workflow changes.
Product
Socket now supports uv.lock files to ensure consistent, secure dependency resolution for Python projects and enhance supply chain security.
Research
Security News
Socket researchers have discovered multiple malicious npm packages targeting Solana private keys, abusing Gmail to exfiltrate the data and drain Solana wallets.