Package Exports
- ts-prims
- ts-prims/big.js
- ts-prims/chars.js
- ts-prims/clob.js
- ts-prims/int.js
- ts-prims/length.js
- ts-prims/memo.js
- ts-prims/prim.js
- ts-prims/text.js
- ts-prims/util.js
- ts-prims/varchar.js
- ts-prims/varint.js
- ts-prims/width.js
Readme
ts-prims
Primitives in Typescript
Offers tooling for creating primitive types in Typescript, as well as some primitive types included to start with.
Table of Contents
- ts-prims
Getting started
Install
npm install ts-primsImport
import { type prim, Prim } from 'ts-prims'Use
Use type prim to create subtypes of primitives such as number:
import { type prim } from 'ts-prims'
type int = prim<number>or even subtypes of subtypes of primitives... etcetera
import { type prim } from 'ts-prims'
type int = prim<number>
type byte = prim<int>Use function Prim to create run-time present versions of the types, just like Javascript gives us String and Number:
import { type prim, Prim } from 'ts-prims'
type int = prim<number>
const Int = Prim('int', Number)
type byte = prim<int>
const Byte = Prim('byte', Int)Use the types that come with ts-prims when interfacing with e.g databases:
import { type int32, type memo, type varchar } from 'ts-prims'
type Article = {
id: int32,
title: varchar<40>,
abstract: varchar<256>,
body: memo
}Creating primitive types
Using PRIM we limit parameters to be of primitive types. With prim we can
create custom primitive types and with Prim we can give these custom types
runtime presence in the form of constructor functions that mimic the built-in
native constructors Boolean, String, Number and BigInt.
PRIM
A primitive is either a boolean, a string, a number or a bigint
export type PRIM = boolean | string | number | bigintWe mainly use this type to limit the temlate parameters' types:
type MyType<P extends PRIM> = ...prim type
prim<P,C> defines a primitive type descending from P.
export type prim<P extends PRIM, C = {}> =
P & supertype<P> & CP must extend PRIM, meaning it must extend one of the primitive
types boolean, string, number and bigint.
The returned type is a tagged intersection type, preventing direct assignment of other primitive values with the same base type, but which are not actually of the same type:
import { type prim } from 'ts-prims'
// int 'extends' number
type int = prim<number>
let i: int = 100 as int
let n: number = 200
n = i // ok
i = n // error
// Type 'number' is not assignable to type 'int'.This helps prevent bugs like this:
// `i` is supposed to be an integer
let i: number = 0.5 // wrong, but no error!prim tags the returned type in a specific way. It sets the type of the tag to
the type of the constructor function used to 'construct' values of the type.
The name 'constructor' for a function that returns a primitive may seem strange to programmers coming from other languages, but Javascript has had built-in 'constructor' functions for the primitives like 'Boolean' and 'String' from its inception and these can be used to construct/convert values to the type they correspond to:
let x: boolean = Boolean(1) // x == true
ts-primsallows you to adopt this pattern for your own types. See below for more information. For now, just know that a 'constructor' for a primitive type is just a function (conventionally with a name starting with an uppercase letter) that you can pass a value and when possible, it will convert that value to the corresponding primitive type.
Using the type of the constructor as the tag type naturally creates an inheritance-like structure which we can use to our benefit:
import { type prim } from 'ts-prims'
// int 'extends' number
type int = prim<number>
// byte 'extends' int
type byte = prim<int>
let i: int = 200 as int
let b: byte = 100 as byte
i = b // ok
b = i // error
// Type 'int' is not assignable to type 'byte'.Typescript understands that byte can be assigned to int, but not
vice-versa.
Typescript checks assignability based on the 'shape' of the type. prim uses
this to create a system that resembles a type hierarchy. But we can go further!
prim accepts a second parameter that we can use to refine the tagging with
extra information. Let's use it to create a simple varchar
type:
import { type prim, type Chars, type chars } from 'ts-prims'
type varchar<N extends Chars> =
prim<string, chars<N>>Chars here is a type containing the possible length in chars for short
strings and chars applies that as a contraint to the new primitive type.
What does this achieve? Lets have a look:
type zipcode = varchar<5>
type title = varchar<32>
let zip: zipcode = '90210' as zipcode
let txt: title = 'Hello primitive types!' as title
txt = zip // ok
zip = txt // error
// Type 'title' is not assignable to type 'zipcode'.Nice! Typescript now understands the difference between varchar<5> and
varchar<32> and it knows that we can safely assign one to the other, but
not the other way around. And we only needed a few lines of code to achieve it!
But lets have a closer look at this. Specifically lets discuss the casting that is happening:
let zip: zipcode = '90210' as zipcodeBy default, '90210' is typed as string and trying to assign it directly to
a variable of type zipcode will give a compile error. That is type-safety for
the win! Casting the literal to zipcode fixes that. But what if the value
was coming from some user input for example and was actually too long?
let zip: zipcode = 'Too long!' as zipcodeYeah that's right. Typescript will happily accept it. For this scenario we need some runtime checks. We will address this in the next section.
For now, a short recap:
primcreates tagged types. Attempting to assign regular primitives to these types gives us a compiler error. That should trigger us to stop and think! We have three options here:
- The error is correct, we are doing something wrong -> fix it
- We can guarantee that the value is actually of the right type -> cast
- We need to check at runtime whether the value is of the right type
When we find that we have situation 1 or 2, we either fix the code or add
a simple cast. But if we are in situation 3, we need to add some runtime
presence of our type with the Prim function.
Prim function
Up till now we just worked with types and everything we did will be erased in the build phase in a process called type erasure.
But sometimes you want your types to have presence at runtime as well, so you
can for example perform validation and conversion on the values. The Prim
function helps us achieve just that in a consistent and convenient way.
export const Prim: PrimFactory = <P extends PRIM> (
name: string,
pc: SuperConstructor<P>,
constraints: Constraint[] | Constraint = []
): PrimConstructor<P>Lets see how it works:
import { type prim, Prim, isInteger } from 'ts-prims'
type int = prim<number>
// creates the constructor function
const Int = Prim<int>(
'int', Number, isInteger
)The call to Prim() creates the function Int, that will call
isInteger on any value given to it, to verify that it is indeed an
integer. Now, we can use Int to perform runtime validation:
let i: int = Int(100) // ok
i = Int(0.5) // runtime error
// TypeError: 0.5 is not assignable to type 'int'.
// Not an integer.The third argument of Prim is an (array of) Constraint. By
default, an empty array is used, but you can supply your own constraints as we
did above and provide custom functions for validation.
This gives us a convenient and reusable pattern to work with primitive types, control assignability between different subtypes of the same base type and give them runtime presence.
And that should be about enough about prim and Prim. You can read further
about some of the constraints that are available to use and how
to write them yourself, have a look at the helper types that
are used under the hood, or you can skip down to the
primitive types included in ts-prims.
Constraints
Constraints are compile-time and/or runtime limitations on the values a certain
type can hold. Included in ts-prims are three constraints that have both
compile-time as well as runtime components.
width
Constraint limiting a type to the given Width W
export type width<W extends Width> =
{ width: Lte<W> }The width constraint can be used on integer numbers of type number or
bigint. It has a compile-time component in the form of the width type
and a runtime component in the form of a widthConstraint:
import { type prim, type width, Prim, widthConstraint }
type int = prim<number, width<4>>
const Int = Prim<int> ('int', Number, widthConstraint(4))This limits the width of int values to 4, which translates to 32 bits.
length
Constrains a type to the given length L.
export type length<L extends Length> =
{ length: Lte<L>, chars: Lte<64> }The length constraint is similar to the width constraint, but expanded to
add 65 extra 'levels' between 0 and 1 by using not one but two properties
to model the constraint:
lengthlimits the type to a subset of 16 broad categoriescharscan be used to further subdivide length1
The length constraint always sets chars to Lte<64>, containing the full range
between length 0 and 1 (64 chars).
The constraint chars can be used to exercise fine-grained control.
chars
Constrains a type to the given length in chars L.
export type chars<L extends Chars> = {
length: Lte<LengthOf<L>>,
chars: L extends Lte<ShortLengthChars[1]> ? Lte<L> : Lte<ShortLengthChars[1]>
}This type allows you to express the length constraint on a short string with very high precision:
import type { prim, chars } from 'ts-prims'
type article_tite = prim<string, chars<64>>
type zipcode = prim<string, chars<5>>
let zip: zipcode = '90210' as zipcode // ok
let title = 'Hello World!' as article_tite // ok
title = zip // ok
zip = title // error
// Type 'article_tite' is not assignable to type 'zipcode'.Use this type in combination with charsConstraint for runtime presence:
export const charsConstraint: CharsConstraint =
<L extends Chars> (l: L) =>
<P extends PRIM> (pc: PrimConstructor<P>, v: PRIM) =>
(typeof v == 'string') && (v.length <= l) ? undefined :
`${display(v)} is not assignable to '${pc.name}'.\n` +
` Length exceeds ${l}.`Use it like this:
import type { prim, chars, Chars } from 'ts-prims'
import { Prim, charsConstraint } from 'ts-prims'
// a varchar with a length in chars `L`
type varchar<L extends Chars> = prim<string, chars<L>>
// the prim type constructor function for `varchar<L>`
const Varchar = <L extends Chars>(l: L) => Prim<varchar<L>>(
`varchar<${l}>`, String, charsConstraint(l)
)
// you can now define varchars with length <= 256 chars:
type zipcode = varchar<5>
const Zipcode = Varchar(5)
let zip: zipcode = Zipcode('90210') // ok
let oops: zipcode = Zipcode('Too long!') // runtime error
// TypeError: "Too long!" is not assignable to 'varchar<5>'.
// Length exceeds 5.superConstraint
Constraint that the primitive type of two types must be equal for them to be assignable to one another. This constraint is implied in the type system and made explicit in the runtime implementation through this object.
export const superConstraint: Constraint =
<P extends PRIM> (pc: PrimConstructor<P>, v: PRIM) =>
typeof v == primTypeOf(pc) ? undefined :
`${display(v)} is not assignable to type '${pc.name}'.\n` +
` Supertypes do not match: ${typeof v}, ${primTypeOf(pc)}.`isInteger
Runtime constraint that checks whether the given value v is an integer.
export const isInteger: Constraint =
<P extends PRIM> (pc: PrimConstructor<P>, v: PRIM) =>
(typeof v == 'bigint') || Number.isInteger(v) ? undefined :
`${display(v)} is not assignable to type '${pc.name}'.\n` +
` Not an integer.`Helper types
supertype
Supertype constraint for a primitive type P
export type supertype<P extends PRIM> =
{ supertype: Constructor<P> }Constructor
A Constructor, in the context of ts-prims is a function that validates/
converts a value to a primitive type.
export type Constructor<P extends PRIM> =
PrimConstructor<P> | NativeConstructor<P>We distinguish between user-defined PrimConstructors and
built-in NativeConstructors.
PrimConstructor
The user-defined constructor for the primitive type P is a
combination of a ToPrim conversion function and Rtti.
export type PrimConstructor<P extends PRIM> =
ToPrim<P> & Rtti<P>NativeConstructor
The native (built-in) constructor for a given primitive type P.
export type NativeConstructor<P extends PRIM> =
P extends boolean ? BooleanConstructor :
P extends string ? StringConstructor :
P extends number ? NumberConstructor :
BigIntConstructorSuperConstructor
The super constructor for a given prim type P is a
Constructor for the PrimTypeOf<P>.
export type SuperConstructor<P extends PRIM> =
Constructor<PrimTypeOf<P>>ToPrim
A type conversion function that converts v to P.
export type ToPrim<P extends PRIM> = (v: PRIM) => PIsPrim
A type guard function that checks whether v is P
export type IsPrim<P extends PRIM> = (v: PRIM) => v is PAsPrim
A type assertion function that asserts that v is P.
export type AsPrim<P extends PRIM> = (v: PRIM) => asserts v is PPrimTypeOf
The prim type of a given prim P is the underlying primitive type, e.g.
number for int32, bigint for big64, string for memo etc.
export type PrimTypeOf<P extends PRIM> =
P extends boolean ? boolean :
P extends string ? string :
P extends number ? number :
bigintRtti
Run-Time Type Information for the primitive type P
export type Rtti<P extends PRIM> = {
name: string
super: SuperConstructor<P>
to: ToPrim<P>
is: IsPrim<P>
as: AsPrim<P>
}Example:
import { type prim, Prim } from 'ts-prims'
type int = prim<number>
const Int = Prim<int>('int', Number)Int will have properties name, super, is, as, to, which are
inspectable and usable at runtime.
PrimFactory
A prim factory creates user-defined constructor functions for user-defined primitive types.
export type PrimFactory =
<P extends PRIM> (
name: string,
pc: SuperConstructor<P>,
constraints: Constraint[] | Constraint
>) => PrimConstructor<P>It is implemented by the Prim function.
Primitive types included
Below are the primitive types included in ts-prims. You can look at their
source code and play with them to learn from them, use them directly in your
code, or use them as the basis to create further refined types specific to
your domain. Enjoy!
Type hierarchy
The types in this project are laid out in this hierarchy:
prim(extendsboolean | string | number | bigint)
clob
Character Large Object
clob type
The base prim type for strings with a very high maximum length of 4294967296 characters (4G).
export type clob =
prim<string, length<15>>Clob constructor
export const Clob = Prim<clob>(
`clob`, String, lengthConstraint(15)
)Clob example
import { type clob, Clob } from 'ts-prims'
// narrow using cast
let x: clob = 'Hello World!' as clob
// or using runtime check by constructor
x = Clob('Checked at runtime')text
For long text.
text type
The base prim type for text with a maximum length of 16777216 characters (16M), length 14.
export type text =
prim<string, length<14>>Text constructor
export const Text = Prim<text>(
`text`, String, [ lengthConstraint(14) ]
)Text example
import { type text, Text } from 'ts-prims'
// narrow using cast
let x: text = 'Hello World!' as text
// or using runtime check by constructor
x = Text('Checked at runtime')memo
For medium text.
memo type
The base prim type for text with a maximum length of 4096 chars (4K)
export type memo =
prim<string, length<11>>Memo constructor
export const Memo = Prim<memo> (
'memo', String, [ lengthConstraint(11) ]
)Memo example
import {
type text, Text,
type memo, Memo
} from 'ts-prims'
let x: memo = Memo('Hello World!')
let y: text = Text('super')
y = x // ok
x = y // error
// Type 'text' is not assignable to type 'memo'.varchar
For high-precision short strings
varchar type
A variable length string with a maximum length of N
export type varchar<N extends Chars> =
prim<string, chars<N>>N must be a literal positive integer number in the range 0 .. 256.
This type is meant to model strings with a limited length like SQL's varchar. For longer strings, use memo if the string length remains below 4K, or text otherwise.
Varchar constructor
export const Varchar =
<N extends Chars> (n: N) => Prim <varchar<N>> (
`varchar<${n}>`, Memo, [ charsConstraint(n) ]
)Varchar example
import {
type text, Text,
type varchar, Varchar
} from 'ts-prims'
// create custom type
type zipcode = varchar<5>
// create custom constructor function
const Zipcode = Varchar(5)
// use them
let zip: zipcode = Zipcode('90210') // ok
let oops: zipcode = Zipcode('Too long!') // runtime error
// TypeError: "Too long!" is not of type 'varchar<5>'
let txt = Text('base')
txt = zip // ok
zip = txt // error
// Type 'text' is not assignable to type 'zipcode'.varint
Fixed variable-width integer type
export type VARINT = number | bigintvarint type
Low-level 'fixed variable-width' signed integer type.
This integer type allows for a wide range of bit widths to be supported,
from 8 to 4096. It is 'fixed' variable-width because this specification
has selected 16 different possible widths and those are the only options
available. Of course, lower width numbers always 'fit' in the higher
width numbers, but not vice versa.
export type varint <W extends Width> =
prim<IntegerType<W>, width<W>>IntType automatically selects the smallest underlying type that will fit:
type X = varint<4> // type X = number & ....
type Y = varint<8> // type Y = bigint & ...This is a low-level type.
Prefer int and big in stead.
Varint constructor
Returns the prim constructor for the varint with the given Width W.
export const Varint = <W extends Width> (w:W) => Prim<varint<W>> (
`varint<${w}>`, integerType(w), [ isInteger, widthConstraint(w) ]
)This constructor function validates that the given value v is an integer and
that it is within the range of the width w.
Varint example
type X = varint<4> // type X = number & ....
type Y = varint<7> // type Y = number & ...
type Z = varint<8> // type Z = bigint & ...
let x: X = 10 as X
let y: Y = 20 as Y
let z: Z = 30n as Z
y = x
x = y // Type 'Y' is not assignable to type 'X'.
y = x
y = z // Type 'Z' is not assignable to type 'Y'.
z = y // Type 'Y' is not assignable to type 'Z'.
type byte = varint<1>
const Byte = Varint(1)
let b: byte = Byte(250) // runtime error
// TypeError: 250 is not assignable to 'varint<1>'.
// Not in range -128 .. 127.int
Low width integers
int type
Int type with low int width W.
export type int <W extends LowWidth = 7> =
prim<number, width<W>>Int constructor
export const Int =
<W extends LowWidth = 7> (w:W = 7 as W) => Prim<int<W>> (
`int<${w}>`, Number, [ isInteger, widthConstraint(w) ]
)Int example
type byte = int<1>
// type byte = number & supertype<number> & width<1>
type word = int<2>
// type word = number & supertype<number> & width<2>
let x: byte = 100 as byte
let y: word = 1000 as word
y = x // ok
x = y // error
// Type 'word' is not assignable to type 'byte'.int8
export type int8 = int<_8bit>int16
export type int16 = int<_16bit>int24
export type int24 = int<_24bit>int32
export type int32 = int<_32bit>int40
export type int40 = int<_40bit>int48
export type int48 = int<_48bit>int54
export type int54 = int<_54bit>big
Big integer numbers
big type
Big int type with high int width W.
export type big <W extends Width> =
prim<bigint, width<W>>Big constructor
Returns a constructor for big numbers with the given Width W.
export const Big = <W extends Width> (w:W) => Prim<big<W>> (
`big<${w}>`, BigInt, [ isInteger, widthConstraint(w) ]
)Big example
import type { big, _4Kbit } from 'ts-prims'
type big4k = big<_4Kbit>
// type big4K = bigint & supertype<bigint> & width<15>big64
64-bit integer in the HighWidth (slow) range.
export type big64 = big<_64bit>warning: may degrade performance!
The Javascript platform only supports up to
54-bit integers with the nativenumbertype. Therefore, values of typebig64are unfortunately stored asbigint, which is a variable-width format that supports ints with hundreds or even thousands of bits, but is much slower than nativenumber. If it is possible to useint54instead, prefer that. Most other platforms have native support for64-bit integers, but if you want true cross-platform performance guarantees, stick toint54.
big96
96-bit integer in the HighWidth (slow) range.
export type big96 = big<_96bit>warning: may degrade performance!
Most platforms have no native support for
96-bit numbers. We emulate them, in this case withbigintand on other platforms in similar ways.
big128
128-bit integer in the HighWidth (slow) range.
export type big128 = big<_128bit>warning: may degrade performance!
Most platforms have no native support for
128-bit numbers. We emulate them, in this case withbigintand on other platforms in similar ways.
big160
160-bit integer in the HighWidth (slow) range.
export type big160 = big<_160bit>warning: may degrade performance!
Most platforms have no native support for
160-bit numbers. We emulate them, in this case withbigintand on other platforms in similar ways.
big192
192-bit integer in the HighWidth (slow) range.
export type big192 = big<_192bit>warning: may degrade performance!
Most platforms have no native support for
192-bit numbers. We emulate them, in this case withbigintand on other platforms in similar ways.
big256
256-bit integer in the HighWidth (slow) range.
export type big256 = big<_256bit>warning: may degrade performance!
Most platforms have no native support for
256-bit numbers. We emulate them, in this case withbigintand on other platforms in similar ways.
big512
512-bit integer in the HighWidth (slow) range.
export type big512 = big<_512bit>warning: may degrade performance!
Most platforms have no native support for
512-bit numbers. We emulate them, in this case withbigintand on other platforms in similar ways.
big4K
4096-bit integer in the HighWidth (slow) range.
export type big4K = big<_4Kbit>warning: may degrade performance!
Most platforms have no native support for
4096-bit numbers. We emulate them, in this case withbigintand on other platforms in similar ways.
Utility types
_Lt
The union of all positive numbers from 0 up to,
but not including, N.
export type _Lt <N extends number, A extends number[] = []> =
N extends A['length'] ? A[number] :
_Lt<N, [A['length'], ...A]>N must be small since this type uses recursion to
generate a union type.
You should use type Lt in place of this one, which forces
a safe limit on type parameter N.
Lt
The union of all positive integers Less Than N.
export type Lt <N extends _Lt<256> | 256> = _Lt<N>Returns the union of all positive integers from 0 up to, but not
including, N. Safe version.
type lt8 = Lt<8>
// type lt8 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7This type is limited to only accept small numbers for its N parameter.
This makes this type safe to use. Below the surface it is delegating to
_Lt, the unrestricted (and therefore 'unsafe') version of this type
that actually performs recursion to determine the range of values.
See Lte if you need an end-inclusive range.
Lte
The union of all positive integers Less Than or
Equal to N.
export type Lte<N extends _Lt<256> | 256> = Lt<N> | NReturns the union of all positive integers from 0 up to and
including, N.
type Lte7 = Lte<7>
// type Lte7 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7See Lt if you need an end-exclusive range.
Issues
Please report issues to this projects Git repository on Github: https://github.com/download/ts-prims
Copyright
Copyright 2025 by Stijn de Witt. Some rights reserved.
License
Open source under the permissive MIT