This library is a set of 150+ composable type-level primitives that exists purely for type-level metaprogramming.
Used to build SDKs, schema systems, configuration engines, frameworks, typed DSLs, query builders, or any other library with an API that needs frictionless type inference/ergonomics.
These are just plain TS types that you assemble into larger types. No runtime code here.
You may be familiar with type-fest, ts-toolbelt, ts-essentials, utility-types, and more.
So why does this exist?
Because none of them fit my worfklow.
Because Type-Fest is too generic (serves the entire TS developer pool), and because ts-toolbelt is ancient, supports older TS (3.8+ / 4.x) and uses this weird nomenclature: O.Merge / L.Concat, and more.
I've been building TypeScript apps and libraries for years at this point, kept running into some increasingly weird type-level problems that I can't Google or LLM my way out of.
Eventually, I made this.
Some of these libraries contain equivalents to individual types in here.
That's unavoidable, afterall I didn't invent Zip<N,M> or Equals<U,V>.
Some foundational types are here simply because importing another entire package for one primitive, such as DeepOmit<T, O>, wouldn't make sense.
Other types in here don't even exist elsewhere at all (more below).
Also, I may note that most utility libraries are collections of finished helpers. Someone needs a type, contributes it, and the collection grows horizontally. And over time, the package becomes a broad catalog made to cover as many unrelated projects and use cases as possible.
This is just a vertically integrated type-system that only I use.
Every type in here solves at least one problem I encountered throughout the years.
The library builds itself from its own primitives.
The same primitives are then used to test the library, are also being turned into dedicated testing infrastructure for my other packages, and are used by those packages to construct their own public APIs that use this package.
Let's start with a very simple and small example:
At some point I needed a string that could independently enforce a prefix, something somewhere in the middle, and a suffix.
EnforcedString<P,M,S> does it:
Say you're mapping CSS variables:
import type { EnforcedString } from 'typyx';
type CssVar = EnforcedString<'--'>;
const color: CssVar = '--color-primary'; // valid
const invalid: CssVar = 'color-primary'; // error
Maybe you need a substring constraint instead:
type I18nKey = EnforcedString<string, '.'>;
const label: I18nKey = 'button.label'; // valid
const invalid: I18nKey = 'buttonlabel'; // error
Or all three:
type LocaleKey = EnforcedString<'i18n.', '.', '.label'>;
const label: LocaleKey = 'i18n.button.label'; // valid
const invalid: LocaleKey = 'button.label'; // error
Then there are boolean operators and type-level control flow: And, Or, Not, Xor, Nand, Nor, Xnor, Xand, If, IfEquals, and IfExtends.
They operate directly on boolean types and compose with the predicate layer.
import type {
And,
If,
IsNever,
IsString,
Not,
StringifyPrimitive,
} from 'typyx';
type IsValidInput<T> = And<IsString<T>, Not<IsNever<T>>>;
type Serialize<T> = If<
IsValidInput<T>,
T,
StringifyPrimitive<T>
>;
But you're probably familiar with IfEquals. So what's special?
It can be combined with NotIncluded and Prune.
One time I was building a hyper-complex dispatch system and ran into a problem where configuration shapes depended on multiple axes at once.
If it's one discriminant, just use a union, but 3 different settings changing 3 different regions of the same object becomes a giant union very quickly.
Say you have 3 environments, 3 isolation strategies, and 3 schedule modes.
That's already 27 possible combos.
You can enumerate all 27 object types manually with unions. It would work, kind of.
Or you declare the rules and let the compiler derive the final shape:
import type { IfEquals, NotIncluded, Prune } from 'typyx';
type Environment = 'wasm' | 'container' | 'native';
type Isolation = 'none' | 'cgroup' | 'vm';
type Schedule = 'cron' | 'immediate' | 'manual';
type JobConfig<
Env extends Environment,
Iso extends Isolation,
Sched extends Schedule,
> = Prune<{
execution: IfEquals<
Env,
'wasm',
{
memoryPages: number;
importedModules: string[];
},
IfEquals<
Env,
'container',
{
image: string;
runtime: 'runc' | 'gvisor';
},
NotIncluded
>
>;
isolation: IfEquals<
Iso,
'none',
NotIncluded,
{
quota: IfEquals<
Iso,
'cgroup',
{
cpuShares: number;
memoryLimitBytes: number;
},
NotIncluded
>;
}
>;
schedule: IfEquals<
Sched,
'cron',
{
expression: string;
timezone: string;
},
IfEquals<
Sched,
'immediate',
{
priority: number;
},
NotIncluded
>
>;
}>;
type WasmCronNoIsolation =
JobConfig<'wasm', 'none', 'cron'>;
const job: WasmCronNoIsolation = {
execution: {
memoryPages: 256,
importedModules: ['env', 'wasi_snapshot_preview1'],
},
schedule: {
expression: '*/5 * * * *',
timezone: 'UTC',
},
};
The final type has no isolation key.
No isolation?: never, nor isolation?: undefined. It just doesn't exist, so it doesn't pollute autocomplete or leak an impossible field into the validators.
This is basically how this grew.
A predicate can feed into a boolean operator, which feeds into If. IfEquals can resolve a field to NotIncluded. Prune can remove that field from the final object entirely.
Numeric and string constraints can exist directly inside those computed shapes as you saw in the demo.
Tuple operations can produce unions that are consumed by object transforms. A failure can return a readable payload too.
Speaking of which.
Say you want tuple uniqueness but still want a meaningful error payload instead of a vague never.
import type { UniqueArray } from 'typyx';
type Good = UniqueArray<[1, 2, 3]>;
// readonly [1, 2, 3]
type Bad = UniqueArray<[1, 2, 1]>;
// readonly [1, 2, 'Encountered duplicate element', 1]
Why is this?
For instance, I recently released envyx, an environment checking library built on this foundation.
In envyx I have this type:
type NoDuplicateIgnoredKeys<Ignored extends IgnoreValidationInputShape> =
Ignored extends readonly unknown[]
? NoTupleDuplicates<
Ignored,
'Duplicate ignored environment variable'
>
: unknown;
I'm using NoTupleDuplicates to detect duplicate ignored environment variables.
And UniqueArray is used directly in the public API:
/**
* Local variable names that should be read without `prefix`.
*/
disablePrefix?: UniqueArray<DisabledKeys>;
So if that option is a tuple, I can reject duplicate keys before anything reaches runtime.
This type comes in handy, say you're writing a query builder, for example:
type QueryBuilder<Columns extends readonly string[]> = {
select: Columns & NoTupleDuplicates<Columns, 'Duplicate column selected'>;
};
function select<C extends readonly string[]>(columns: QueryBuilder<C>['select']) {}
// works!
select(['id', 'email', 'created_at']);
// Type Error: Property 'Duplicate column selected: email
select(['id', 'email', 'created_at', 'email']);
envyx also pulls in base everyday utilities EmptyObject, NonEmptyArray, Simplify, UnionToIntersection and non everyday utilties like UnionToTuple, which is often warned against, because union member ordering isn't guaranteed by the compiler. This is technically right (about order), but, when working with object keys, order doesn't matter, DX and zero duplication do.
In the library, without UnionToTuple, users disabling prefixes would have to manually duplicate string keys in a literal array, which is workable when working with 2 keys:
const serverVars = { DATABASE_URL: z.url(), SECRET: z.string() } as const;
// key duplication: annoying, drifts easily
disablePrefix: ['DATABASE_URL', 'SECRET']
But in a modern monorepo, you'd have at least 20. So would go and manually supply every key?
With UnionToTuple<T>, envyx provides a lightweight keys() helper (return Object.keys(s) as UnionToTuple<keyof Schema>) to derive those keys automatically at both runtime and compile time:
// zero duplication
disablePrefix: keys(serverVars)
EmptyObject as simple and mundane as it is, is quite useful, and I use it everywhere.
In many codebases {} is used an empty object which is actually Record<string, never>, which confuses people, but it actually means any value you can look up properties on, so it's NonNullable<unknown>.
Small things like this matter.
Speaking of small things.
With type-fest, if you want a basic branded type, you're forced into an over-engineered Tagged system.
I just want to enforce nominal typing for a string or number, I don't want or need any of this.
Usually in the TS/JS ecosystem it's called Branded or Opaque types. I called it
NewTypebased on Python'sNewType.
A known use case is to use it to differentiate UserId from OrderId (both strings) where object ordering won't save you from same parameter transposition at compile time.
But I had this problem once:
JavaScript has no distinct runtime value for an operation that intentionally produced no value.
A function that returns nothing (typed void in TS) and a function that explicitly returns undefined both end up as undefined at runtime.
Sometimes those are different states, which we need to track.
Say you're tracking values across a functional pipeline. You may need to drop an internal unit result while preserving an explicitly supplied undefined as actual data.
A private symbol/sentinel value can mark the distinction at runtime, while NewType preserves the same distinction at compile time:
const NO_VALUE = Symbol('internal:no-value');
type NoValue = NewType<'NoValue', void>;
type R<T> = {
value: T;
[NO_VALUE]?: true;
};
const unit: R<NoValue> = {
value: undefined as NoValue,
[NO_VALUE]: true,
};
const explicitUndefined: R<undefined> = {
value: undefined,
};
Both contain undefined, but they're no longer interchangeable.
Some problems may entail that you should have a configuration with numbers like: PositiveInteger, NegativeInteger, PositiveFloat, NegativeFloat, PositiveRange, Odd, Even, and string equivalents such as NegativeFloatString<'-82739.283293237'>.
This works (non recursive):
import type { IsNegative } from 'typyx';
type Result = IsNegative<10000000000000000000000000000>;
// false
Odd and Even exist almost everywhere though, added here for convenience:
import type { Odd } from 'typyx';
function takesOdd<N extends number>(value: Odd<N>) {}
takesOdd(3); // valid
takesOdd(4); // error
But anyway, you might want to check the docs.
This is a types only library, there's no JS in the final bundle, so pass the --save-dev or -D flag:
npm
npm i -D typyx
pnpm
pnpm i -D typyx
Releases are OIDC signed and published through GitHub Actions with npm provenance.
[!IMPORTANT] Requires TypeScript
v5.0+. Every type here is compile-time tested in CI from 5.0 through 6.0 on every push.
Check out the full API reference for detailed usage examples and docs.
The best way to understand how these types work (aside from docs) is to check how they're tested.
Assign<Obj, ObjArr> - Copies all enumerable own properties from one target object to a source array of objects.FilterBy<Obj, P> - Filters keys from the object type Obj based on a specified predicate P.Flip<Obj> - Flips the keys and values of an object type Obj.ImmutableKeys<Obj> - Retrieves the readonly keys from an object type Obj.Keys<T> - Retrieves the union of keys of a type T.KeysOfUnion<T> - Extracts the union of keys from a union of object types.KeysToValues<Obj> - Creates a reverse mapping from values to keys for a simple object type.MakeOptional<T, K> - Makes the specified keys K optional while preserving the original modifiers of all other keys.MakeRequired<T, K> - Makes the specified keys K required while preserving the original modifiers of all other keys.Methods<Obj> - Gets the literal names of keys that are methods in an object type Obj.MutableKeys<Obj> - Retrieves the mutable keys from an object type Obj.NonRequiredKeys<Obj> - Returns all non-required keys of an object type Obj.NotIncluded - Marker type used with deep pruning utilities to completely omit fields.OmitByType<Obj, T> - Omits properties from Obj whose types are assignable to T.OmitCommonKeys<Obj1, Obj2> - Omits any keys shared by Obj1 and Obj2.OmitExactlyByTypeDeep<Obj, T> - Deeply omits properties whose types exactly match T.PickByType<Obj, T> - Picks properties from Obj whose types are assignable to T.PickCommonKeys<Obj1, Obj2> - Gets the common keys between two object types.PickExactlyByType<Obj, T> - Picks properties from Obj whose types exactly match T.Properties<Obj> - Gets the literal names of keys that are non-method properties in an object type Obj.Prune<T, N = NotIncluded> - Recursively omits properties of type N from T.ReplaceKeys<Obj1, P, Obj2> - Replaces properties P in Obj1 with the corresponding properties from Obj2.RequiredKeys<Obj> - Gets the required keys of an object type Obj.Vals<Obj> - Gets the union of value types from an object type.DeepAwaited<T> - Recursively resolves all nested Promise types to their underlying values.DeepImmutable<Obj> - Recursively makes every property in Obj readonly.DeepMutable<Obj> - Recursively removes readonly from every property in Obj.DeepNotRequired<Obj> - Recursively makes all properties optional.DeepOmit<Obj, P> - Recursively omits specified nested properties from an object based on path P.DeepPick<Obj, P> - Deeply picks properties from a nested object based on path P.DeepRequired<Obj> - Recursively makes all properties required.DeepToPrimitive<Obj> - Recursively transforms an object type into one whose properties are their primitive counterparts.IsDeepImmutable<Obj> - Checks if all nested properties of Obj are immutable.IsDeepMutable<Obj> - Checks if all nested properties of Obj are mutable.IsDeepNotRequired<Obj> - Checks if all nested properties of Obj are optional.IsDeepRequired<Obj> - Checks if all nested properties of Obj are required.Paths<Obj> - Generates all possible dot-separated key paths from a nested object type.ExclusiveUnion<T> - Creates a union type where each variant keeps its own required properties while excluding incompatible ones.KeysOfUnion<T> - Extracts the full key union across a union of object types.NotAssignableTo<U, V> - Excludes all members of U that are assignable to V.TupleToUnion<T> - Converts a tuple type into a union type.UnionToIntersection<U> - Converts a union type into an intersection type.UnionToTuple<T> - Converts a union type into a tuple type.Append<Arr, Item> - Adds an item to the end of a tuple.EitherOneOrMany<T> - Represents either a single value of type T or an array of T.ArrayFilter<T, P> - Filters elements from an array type T based on a predicate type P.ArrayIntersection<Arr> - Computes the intersection of the element types shared by every tuple or array in Arr.Head<Arr> - Gets the first element of a tuple.IsArrayIncludesTypeof<Arr, T> - Checks whether an array type Arr is assignable to T[].Last<Arr> - Gets the last element of a tuple.NonEmptyArray<T> - Represents an array containing at least one element of type T.Pop<Arr> - Removes the last element of a tuple.Prepend<Arr, Item> - Adds an item to the start of a tuple.SizedTuple<T, N> - Creates a tuple of length N where each element is of type T.NoTupleDuplicates<T, Message> - Enforces tuple element uniqueness at compile time and surfaces typed error messages for duplicate values.TupleDuplicates<T> - Extracts a union of all duplicate element types present within a tuple.ArrayMax<Arr> - Extracts the maximum numeric value in a given array of numeric types.ArrayMin<Arr> - Extracts the minimum numeric value in a given array of numeric types.Tail<Arr> - Removes the first element of a tuple.Transpose<M> - Transposes a matrix (2D array) by converting rows into columns and columns into rows.Tuple<T> - Narrows T to a tuple type and rejects regular arrays.UniqueArray<T> - Creates a unique array type from an array type T.Zip<L, L1> - Pairs elements from two tuples by index into a tuple of pairs.CapitalizeFirst<T> - Capitalizes the first character of a string literal type.EnforcedString<Prefix, Contains, Suffix> - Restricts a string using optional prefix, substring, and suffix constraints.EqualStrlen<S1, S2> - Checks whether two strings have the same length.FilledString<S> - Errors on an empty string literal ''.NumerifyString<S> - Converts a string literal into a Numeric when possible.StringEndsWith<S, E> - Checks whether a string S ends with E.StringStartsWith<S, St> - Checks whether a string S starts with St.StringifyPrimitive<P> - Turns a primitive value type into its string representation.StrBetween<S, Min, Max> - Ensures a string S has a length within [Min, Max].Strlen<S> - Computes the length of a string S.StrMax<S, Max> - Ensures that a string S has length less than or equal to Max.StrMin<S, Min> - Ensures that a string S has length greater than or equal to Min.Abs<N> - Gets the absolute value of a Numeric.Even<T> - Represents an even Numeric.Float<N> - Type representing a float.Integer<N> - Represents an integer.IsFloat<N> - Checks if a given numeric type is a float.IsInteger<N> - Checks if a given numeric type is an integer.IsNegative<N> - Checks if a numeric type is negative.IsNegativeFloat<N> - Checks if a numeric type is a negative float.IsNegativeInteger<N> - Checks if a numeric type is a negative integer.IsPositive<N> - Checks if a numeric type is positive.IsPositiveFloat<N> - Checks if a numeric type is a positive float.IsPositiveInteger<N> - Checks if a numeric type is a positive integer.Max<A, B> - Gets the larger of two Numeric literals.Min<A, B> - Gets the smaller of two Numeric literals.NegativeFloat<N> - Represents a negative Float<N>.NegativeFloatString<S> - Represents a negative float parsed from a string.NegativeInteger<N> - Represents a negative Integer<N>.NegativeIntegerString<S> - Represents a negative integer parsed from a string.Numeric - Represents number | bigint.Odd<T> - Represents an odd Numeric.PositiveFloat<N> - Represents a positive Float<N>.PositiveFloatString<S> - Represents a positive float parsed from a string.PositiveInteger<N> - Represents a positive Integer<N>.PositiveIntegerString<S> - Represents a positive integer parsed from a string.PositiveRange<N, M> - Represents a range of positive integers from N to M inclusive.And<B1, B2> - Logical AND between two boolean types.Equals<X, Y> - Checks if two types are exactly equal.If<C, Do, Else> - Resolves to Do if C is true, otherwise Else.IfEquals<T, P, Do, Else> - Resolves to Do if T equals P, otherwise Else.IfExtends<T, P, Do, Else> - Resolves to Do if T extends P, otherwise Else.Nand<B1, B2> - Logical NAND between two boolean types.Nor<A, B> - Logical NOR between two boolean types.Not<B> - Negates a boolean type.Or<B1, B2> - Logical OR between two boolean types.Xand<A, B> - Logical XAND between two boolean types.Xnor<A, B> - Logical XNOR between two boolean types.Xor<B1, B2> - Logical XOR between two boolean types.Extends<T, U> - Evaluates whether type T is assignable to type U.FalsyProperties<T> - Extracts falsy properties from an object type T.Is<T, U> - Checks if two types are exactly identical.IsAnyFunction<T> - Checks if T is an arbitrary function type.IsArray<T> - Checks if T is an array type.IsBigInt<T> - Checks if T is a bigint.IsBoolean<T> - Checks if T is a boolean.IsExactlyAny<T> - Checks if T is exactly any.IsExactlyBigInt<T> - Checks if T is exactly bigint.IsExactlyNumber<T> - Checks if T is exactly number.IsExactlyString<T> - Checks if T is exactly string.IsExactlySymbol<T> - Checks if T is exactly symbol.IsExactlyUnknown<T> - Checks if T is exactly unknown.IsFalsy<T> - Checks if a given type T is Falsy.IsFunction<T> - Checks if a given type T is a function.IsNever<T> - Checks if a type resolves to never.IsNewable<T> - Checks if a type T is Newable.IsNot<T, U> - Checks if two types are not identical.IsNullable<T> - Checks if a type T is Nullable.IsNumber<T> - Checks if a type T is a number.IsNumeric<T> - Checks if a type T is Numeric.IsObject<T> - Checks if a type T qualifies as an object.IsString<T> - Checks if a type T is a string.IsSymbol<T> - Checks if a type T is a symbol.IsTruthy<T> - Checks if a type T resolves to a truthy value.IsUnknown<T> - Checks if a type T is assignable to unknown.TestType<T1, T2, Expected> - Tests whether T1 and T2 match the expected relationship.TruthyProperties<T> - Extracts truthy properties from an object type T.AnyFunction - Represents any function accepting any arguments and returning any value.EmptyObject - Represents a non-nullish object-like value.ExcludeNull<T> - Excludes null from a type T.ExcludeNullable<T> - Excludes Nullable from a type T.ExcludeUndefined<T> - Excludes undefined from a type T.Falsy - Represents JavaScript falsy values.Maybe<T> - Represents a type that may be Nullable.MaybeUndefined<T> - Represents a type that may be undefined.MaybeUnknown<T> - Represents a type widened with unknown.Message<T> - Used to surface readable error messages instead of never.Newable - Represents constructor functions that can be invoked with new.NewType<New, Base> - Creates a branded type derived from an existing base type.Nullable - Represents a type that can be null or undefined.Optional<T> - Represents a type that may be null, similar to Python's Optional or Rust's Option.PartialExcept<T, P> - Makes all properties in T optional except those in P, which remain required.Primitive - Represents all JavaScript primitive types.Simplify<T> - Flattens and normalizes a type for better readability.UnknownFunction - Represents a function accepting unknown arguments and returning unknown.See releases.
MIT © @rccyx