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 me walk you through some use cases.
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'; // errorMaybe you need a substring constraint instead:
type I18nKey = EnforcedString<string, '.'>;
const label: I18nKey = 'button.label'; // valid
const invalid: I18nKey = 'buttonlabel'; // errorOr all three:
type LocaleKey = EnforcedString<'i18n.', '.', '.label'>;
const label: LocaleKey = 'i18n.button.label'; // valid
const invalid: LocaleKey = 'button.label'; // errorThen 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>;
// falseOdd 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); // errorBut 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 typyxpnpm
pnpm i -D typyxReleases 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 typeObjbased on a specified predicateP.Flip<Obj>- Flips the keys and values of an object typeObj.ImmutableKeys<Obj>- Retrieves thereadonlykeys from an object typeObj.Keys<T>- Retrieves the union of keys of a typeT.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 keysKoptional while preserving the original modifiers of all other keys.MakeRequired<T, K>- Makes the specified keysKrequired while preserving the original modifiers of all other keys.Methods<Obj>- Gets the literal names of keys that are methods in an object typeObj.MutableKeys<Obj>- Retrieves the mutable keys from an object typeObj.NonRequiredKeys<Obj>- Returns all non-required keys of an object typeObj.NotIncluded- Marker type used with deep pruning utilities to completely omit fields.OmitByType<Obj, T>- Omits properties fromObjwhose types are assignable toT.OmitCommonKeys<Obj1, Obj2>- Omits any keys shared byObj1andObj2.OmitExactlyByTypeDeep<Obj, T>- Deeply omits properties whose types exactly matchT.PickByType<Obj, T>- Picks properties fromObjwhose types are assignable toT.PickCommonKeys<Obj1, Obj2>- Gets the common keys between two object types.PickExactlyByType<Obj, T>- Picks properties fromObjwhose types exactly matchT.Properties<Obj>- Gets the literal names of keys that are non-method properties in an object typeObj.Prune<T, N = NotIncluded>- Recursively omits properties of typeNfromT.ReplaceKeys<Obj1, P, Obj2>- Replaces propertiesPinObj1with the corresponding properties fromObj2.RequiredKeys<Obj>- Gets the required keys of an object typeObj.Vals<Obj>- Gets the union of value types from an object type.
DeepAwaited<T>- Recursively resolves all nestedPromisetypes to their underlying values.DeepImmutable<Obj>- Recursively makes every property inObjreadonly.DeepMutable<Obj>- Recursively removesreadonlyfrom every property inObj.DeepNotRequired<Obj>- Recursively makes all properties optional.DeepOmit<Obj, P>- Recursively omits specified nested properties from an object based on pathP.DeepPick<Obj, P>- Deeply picks properties from a nested object based on pathP.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 ofObjare immutable.IsDeepMutable<Obj>- Checks if all nested properties ofObjare mutable.IsDeepNotRequired<Obj>- Checks if all nested properties ofObjare optional.IsDeepRequired<Obj>- Checks if all nested properties ofObjare 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 ofUthat are assignable toV.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 typeTor an array ofT.ArrayFilter<T, P>- Filters elements from an array typeTbased on a predicate typeP.ArrayIntersection<Arr>- Computes the intersection of the element types shared by every tuple or array inArr.Head<Arr>- Gets the first element of a tuple.IsArrayIncludesTypeof<Arr, T>- Checks whether an array typeArris assignable toT[].Last<Arr>- Gets the last element of a tuple.NonEmptyArray<T>- Represents an array containing at least one element of typeT.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 lengthNwhere each element is of typeT.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>- NarrowsTto a tuple type and rejects regular arrays.UniqueArray<T>- Creates a unique array type from an array typeT.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 aNumericwhen possible.StringEndsWith<S, E>- Checks whether a stringSends withE.StringStartsWith<S, St>- Checks whether a stringSstarts withSt.StringifyPrimitive<P>- Turns a primitive value type into its string representation.StrBetween<S, Min, Max>- Ensures a stringShas a length within[Min, Max].Strlen<S>- Computes the length of a stringS.StrMax<S, Max>- Ensures that a stringShas length less than or equal toMax.StrMin<S, Min>- Ensures that a stringShas length greater than or equal toMin.
Abs<N>- Gets the absolute value of aNumeric.Even<T>- Represents an evenNumeric.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 twoNumericliterals.Min<A, B>- Gets the smaller of twoNumericliterals.NegativeFloat<N>- Represents a negativeFloat<N>.NegativeFloatString<S>- Represents a negative float parsed from a string.NegativeInteger<N>- Represents a negativeInteger<N>.NegativeIntegerString<S>- Represents a negative integer parsed from a string.Numeric- Representsnumber | bigint.Odd<T>- Represents an oddNumeric.PositiveFloat<N>- Represents a positiveFloat<N>.PositiveFloatString<S>- Represents a positive float parsed from a string.PositiveInteger<N>- Represents a positiveInteger<N>.PositiveIntegerString<S>- Represents a positive integer parsed from a string.PositiveRange<N, M>- Represents a range of positive integers fromNtoMinclusive.
And<B1, B2>- LogicalANDbetween two boolean types.Equals<X, Y>- Checks if two types are exactly equal.If<C, Do, Else>- Resolves toDoifCistrue, otherwiseElse.IfEquals<T, P, Do, Else>- Resolves toDoifTequalsP, otherwiseElse.IfExtends<T, P, Do, Else>- Resolves toDoifTextendsP, otherwiseElse.Nand<B1, B2>- LogicalNANDbetween two boolean types.Nor<A, B>- LogicalNORbetween two boolean types.Not<B>- Negates a boolean type.Or<B1, B2>- LogicalORbetween two boolean types.Xand<A, B>- LogicalXANDbetween two boolean types.Xnor<A, B>- LogicalXNORbetween two boolean types.Xor<B1, B2>- LogicalXORbetween two boolean types.
Extends<T, U>- Evaluates whether typeTis assignable to typeU.FalsyProperties<T>- Extracts falsy properties from an object typeT.Is<T, U>- Checks if two types are exactly identical.IsAnyFunction<T>- Checks ifTis an arbitrary function type.IsArray<T>- Checks ifTis an array type.IsBigInt<T>- Checks ifTis abigint.IsBoolean<T>- Checks ifTis aboolean.IsExactlyAny<T>- Checks ifTis exactlyany.IsExactlyBigInt<T>- Checks ifTis exactlybigint.IsExactlyNumber<T>- Checks ifTis exactlynumber.IsExactlyString<T>- Checks ifTis exactlystring.IsExactlySymbol<T>- Checks ifTis exactlysymbol.IsExactlyUnknown<T>- Checks ifTis exactlyunknown.IsFalsy<T>- Checks if a given typeTisFalsy.IsFunction<T>- Checks if a given typeTis a function.IsNever<T>- Checks if a type resolves tonever.IsNewable<T>- Checks if a typeTisNewable.IsNot<T, U>- Checks if two types are not identical.IsNullable<T>- Checks if a typeTisNullable.IsNumber<T>- Checks if a typeTis anumber.IsNumeric<T>- Checks if a typeTisNumeric.IsObject<T>- Checks if a typeTqualifies as an object.IsString<T>- Checks if a typeTis astring.IsSymbol<T>- Checks if a typeTis asymbol.IsTruthy<T>- Checks if a typeTresolves to a truthy value.IsUnknown<T>- Checks if a typeTis assignable tounknown.TestType<T1, T2, Expected>- Tests whetherT1andT2match the expected relationship.TruthyProperties<T>- Extracts truthy properties from an object typeT.
AnyFunction- Represents any function accepting any arguments and returning any value.EmptyObject- Represents a non-nullish object-like value.ExcludeNull<T>- Excludesnullfrom a typeT.ExcludeNullable<T>- ExcludesNullablefrom a typeT.ExcludeUndefined<T>- Excludesundefinedfrom a typeT.Falsy- Represents JavaScript falsy values.Maybe<T>- Represents a type that may beNullable.MaybeUndefined<T>- Represents a type that may beundefined.MaybeUnknown<T>- Represents a type widened withunknown.Message<T>- Used to surface readable error messages instead ofnever.Newable- Represents constructor functions that can be invoked withnew.NewType<New, Base>- Creates a branded type derived from an existing base type.Nullable- Represents a type that can benullorundefined.Optional<T>- Represents a type that may benull, similar to Python'sOptionalor Rust'sOption.PartialExcept<T, P>- Makes all properties inToptional except those inP, which remain required.Primitive- Represents all JavaScript primitive types.Simplify<T>- Flattens and normalizes a type for better readability.UnknownFunction- Represents a function acceptingunknownarguments and returningunknown.
See releases.
MIT © @rccyx