JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q33340F
  • License MIT

Typescript Primitives

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

Getting started

Install

npm install ts-prims

Import

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 | bigint

We 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> & C

P 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-prims allows 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 zipcode

By 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 zipcode

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

prim creates 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:

  1. The error is correct, we are doing something wrong -> fix it
  2. We can guarantee that the value is actually of the right type -> cast
  3. 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:

  • length limits the type to a subset of 16 broad categories
  • chars can be used to further subdivide length 1

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

SuperConstructor

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) => P

IsPrim

A type guard function that checks whether v is P

export type IsPrim<P extends PRIM> = (v: PRIM) => v is P

AsPrim

A type assertion function that asserts that v is P.

export type AsPrim<P extends PRIM> = (v: PRIM) => asserts v is P

PrimTypeOf

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

Rtti

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:

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'.

See: text, memo

varint

Fixed variable-width integer type

export type VARINT = number | bigint

varint 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 native number type. Therefore, values of type big64 are unfortunately stored as bigint, which is a variable-width format that supports ints with hundreds or even thousands of bits, but is much slower than native number. If it is possible to use int54 instead, prefer that. Most other platforms have native support for 64-bit integers, but if you want true cross-platform performance guarantees, stick to int54.

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 with bigint and 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 with bigint and 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 with bigint and 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 with bigint and 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 with bigint and 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 with bigint and 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 with bigint and 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 | 7

This 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> | N

Returns 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 | 7

See 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 2025 by Stijn de Witt. Some rights reserved.

License

Open source under the permissive MIT