Typescript Primitives
npm install ts-primsOffers tooling for
creating primitive types
in Typescript, as well as some
primitive types included
to start with.
- ts-prims
- Table of Contents
- Getting started
- Install
- Import
- Use
- Creating primitive types
- PRIM
- prim type
- Prim function
- Constraints
- width
- length
- chars
- superConstraint
- isInteger
- Helper types
- supertype
- Constructor
- PrimConstructor
- NativeConstructor
- SuperConstructor
- ToPrim
- IsPrim
- AsPrim
- PrimTypeOf
- Rtti
- PrimFactory
- Primitive types included
- Type hierarchy
- clob
- clob type
- Clob constructor
- Clob example
- text
- text type
- Text constructor
- Text example
- memo
- memo type
- Memo constructor
- Memo example
- varchar
- varchar type
- Varchar constructor
- Varchar example
- varint
- varint type
- Varint constructor
- Varint example
- int
- int type
- Int constructor
- Int example
- int8
- int16
- int24
- int32
- int40
- int48
- int54
- big
- big type
- Big constructor
- Big example
- big64
- big96
- big128
- big160
- big192
- big256
- big512
- big4K
- Utility types
- \_Lt
- Lt
- Lte
- Issues
- Copyright
- License
``console`
npm install ts-prims
`ts`
import { type prim, Prim } from 'ts-prims'
Use type prim to create subtypes of primitives such as number:
`ts
import { type prim } from 'ts-prims'
type int = prim
`
or even subtypes of subtypes of primitives... etcetera
`ts
import { type prim } from 'ts-prims'
type int = prim
type byte = prim
`
Use function Prim to create run-time present versions of the types, just like Javascript gives us String and Number:
`ts
import { type prim, Prim } from 'ts-prims'
type int = prim
const Int = Prim('int', Number)
type byte = prim
const Byte = Prim('byte', Int)
`
Use the types that come with ts-prims when interfacing with e.g databases:
`ts
import { type int32, type memo, type varchar } from 'ts-prims'
type Article = {
id: int32,
title: varchar<40>,
abstract: varchar<256>,
body: memo
}
`
Using PRIM we limit parameters to be of primitive types. With prim we canPrim
create custom primitive types and with we can give these custom typesBoolean
runtime presence in the form of constructor functions that mimic the built-in
native constructors , String, Number and BigInt.
A primitive is either a boolean, a string, a number or a bigint
`ts`
export type PRIM = boolean | string | number | bigint
We mainly use this type to limit the temlate parameters' types:
` = ...ts`
type MyType
prim
defines a primitive type descending from P.
` = & Cts`
export type prim
P & supertype
P must extend PRIM, meaning it must extend one of the primitiveboolean
types , 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:
`ts
import { type prim } from 'ts-prims'
// int 'extends' number
type int = prim
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:
`tsi
// 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:
>
> `ts`
> 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:
`ts
import { type prim } from 'ts-prims'
// int 'extends' number
type int = prim
// byte 'extends' int
type byte = prim
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 usesprim
this to create a system that resembles a type hierarchy. But we can go further! accepts a second parameter that we can use to refine the tagging withvarchar
extra information. Let's use it to create a simple
type:
`ts
import { type prim, type Chars, type chars } from 'ts-prims'
type varchar
prim
`
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:
`ts`
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> andvarchar<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:
`ts`
let zip: zipcode = '90210' as zipcode
By default, '90210' is typed as string and trying to assign it directly tozipcode
a variable of type will give a compile error. That is type-safety forzipcode
the win! Casting the literal to fixes that. But what if the value
was coming from some user input for example and was actually too long?
`ts`
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.
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.
` ( ,ts`
export const Prim: PrimFactory =
name: string,
pc: SuperConstructor
constraints: Constraint[] | Constraint = []
): PrimConstructor
Lets see how it works:
`ts
import { type prim, Prim, isInteger } from 'ts-prims'
type int = prim
// creates the constructor function
const Int = Prim
'int', Number, isInteger
)
`
The call to Prim() creates the function Int, that will callisInteger on any value given to it, to verify that it is indeed anInt
integer. Now, we can use to perform runtime validation:
`ts`
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 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
`ts`
export type width
{ width: Lte
The width constraint can be used on integer numbers of type number orbigint. It has a compile-time component in the form of the width typewidthConstraint
and a runtime component in the form of a :
`ts
import { type prim, type width, Prim, widthConstraint }
type int = prim
const Int = Prim
`
This limits the width of int values to 4, which translates to 32 bits.
#### length
Constrains a type to the given length L.
`ts`
export type length
{ length: Lte
The length constraint is similar to the width constraint, but expanded to0
add 65 extra 'levels' between and 1 by using not one but two properties
to model the constraint:
* length limits the type to a subset of 16 broad categorieschars
* can be used to further subdivide length 1
The length constraint always sets chars to Lte<64>, containing the full range0
between length 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.
`ts`
export type chars
length: Lte
chars: L extends Lte
}
This type allows you to express the length constraint on a short string
with very high precision:
`ts
import type { prim, chars } from 'ts-prims'
type article_tite = prim
type zipcode = prim
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:
` (pc: PrimConstructor , v: PRIM) =>ts${display(v)} is not assignable to '${pc.name}'.\n
export const charsConstraint: CharsConstraint =
(typeof v == 'string') && (v.length <= l) ? undefined :
+ Length exceeds ${l}.
`
Use it like this:
`tsL
import type { prim, chars, Chars } from 'ts-prims'
import { Prim, charsConstraint } from 'ts-prims'
// a varchar with a length in chars varchar
type varchar
// the prim type constructor function for varchar<${l}>
const Varchar =
, 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.
(pc: PrimConstructor , v: PRIM) =>
`ts${display(v)} is not assignable to type '${pc.name}'.\n
export const superConstraint: Constraint =
typeof v == primTypeOf(pc) ? undefined :
+ Supertypes do not match: ${typeof v}, ${primTypeOf(pc)}.
`
#### isInteger
Runtime constraint that checks whether the given value v is an integer.
` (pc: PrimConstructor , v: PRIM) =>ts${display(v)} is not assignable to type '${pc.name}'.\n
export const isInteger: Constraint =
(typeof v == 'bigint') || Number.isInteger(v) ? undefined :
+ Not an integer.
`
#### supertype
Supertype constraint for a primitive type P
` = }ts`
export type supertype
{ supertype: Constructor
#### Constructor
A Constructor, in the context of ts-prims is a function that validates/
converts a value to a primitive type.
` = | NativeConstructorts`
export type Constructor
PrimConstructor
We distinguish between user-defined PrimConstructors and
built-in NativeConstructors.
#### PrimConstructor
The user-defined constructor for the primitive type P is aToPrim
combination of a conversion function and Rtti.
` = & Rttits`
export type PrimConstructor
ToPrim
#### NativeConstructor
The native (built-in) constructor for a given primitive type P.
` =ts`
export type NativeConstructor
P extends boolean ? BooleanConstructor :
P extends string ? StringConstructor :
P extends number ? NumberConstructor :
BigIntConstructor
#### SuperConstructor
The super constructor for a given prim type P is aConstructor for the PrimTypeOf
.
` =ts`
export type SuperConstructor
Constructor
#### ToPrim
A type conversion function that converts v to P.
` = (v: PRIM) => Pts`
export type ToPrim
#### IsPrim
A type guard function that checks whether v is P
` = (v: PRIM) => v is Pts`
export type IsPrim
#### AsPrim
A type assertion function that asserts that v is P.
` = (v: PRIM) => asserts v is Pts`
export type AsPrim
#### 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.
` =ts`
export type PrimTypeOf
P extends boolean ? boolean :
P extends string ? string :
P extends number ? number :
bigint
#### Rtti
Run-Time Type Information for the primitive type P
` = {ts`
export type Rtti
name: string
super: SuperConstructor
to: ToPrim
is: IsPrim
as: AsPrim
}
Example:
`ts`
import { type prim, Prim } from 'ts-prims'
type int = prim
const Int = Prim
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.
` ( ,ts`
export type PrimFactory =
name: string,
pc: SuperConstructor
constraints: Constraint[] | Constraint
>) => PrimConstructor
It is implemented by the Prim function.
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!
The types in this project are laid out in this hierarchy:
* prim (extends boolean | string | number | bigint)varint
* (extends number | bigint)boolean
* string
* clob
* text
* memo
* varchar
* number
* int
* int8
* int16
* int24
* int32
* int40
* int48
* int54
* bigint
* big
* big64
* big96
* big128
* big160
* big192
* big256
* big512
* big4K
*
Character Large Object
#### clob type
The base prim type for strings with a very high maximum length of
4294967296 characters (4G).
`ts`
export type clob =
prim
#### Clob constructor
`tsclob
export const Clob = Prim
, String, lengthConstraint(15)`
)
#### Clob example
`ts
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')
`
For long text.
#### text type
The base prim type for text with a maximum length of 16777216 characters (16M), length 14.
`ts`
export type text =
prim
#### Text constructor
`tstext
export const Text = Prim
, String, [ lengthConstraint(14) ]`
)
#### Text example
`ts
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')
`
For medium text.
#### memo type
The base prim type for text with a maximum length of 4096 chars (4K)
`ts`
export type memo =
prim
#### Memo constructor
`ts`
export const Memo = Prim
'memo', String, [ lengthConstraint(11) ]
)
#### Memo example
`ts
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'.
`
For high-precision short strings
#### varchar type
A variable length string with a maximum length of N
`ts`
export type varchar
prim
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
`tsvarchar<${n}>
export const Varchar =
, Memo, [ charsConstraint(n) ]`
)
#### Varchar example
`ts
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'.
`
Fixed variable-width integer type
`ts`
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 specification16
has selected 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.
`ts`
export type varint
prim
IntType automatically selects the smallest underlying type that will fit:
`ts`
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.
`tsvarint<${w}>
export const Varint =
, integerType(w), [ isInteger, widthConstraint(w) ]`
)
This constructor function validates that the given value v is an integer andw
that it is within the range of the width .
#### Varint example
`ts
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.
`
Low width integers
#### int type
Int type with low int width W.
`ts`
export type int
prim
#### Int constructor
`tsint<${w}>
export const Int =
, Number, [ isInteger, widthConstraint(w) ]`
)
#### Int example
`ts
type byte = int<1>
// type byte = number & supertype
type word = int<2>
// type word = number & supertype
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
`ts`
export type int8 = int<_8bit>
#### int16
`ts`
export type int16 = int<_16bit>`
#### int24ts`
export type int24 = int<_24bit>`
#### int32ts`
export type int32 = int<_32bit>`
#### int40ts`
export type int40 = int<_40bit>`
#### int48ts`
export type int48 = int<_48bit>`
#### int54ts`
export type int54 = int<_54bit>
Big integer numbers
#### big type
Big int type with high int width W.
`ts`
export type big
prim
#### Big constructor
Returns a constructor for big numbers with the given Width W.
`tsbig<${w}>
export const Big =
, BigInt, [ isInteger, widthConstraint(w) ]`
)
#### Big example
`ts
import type { big, _4Kbit } from 'ts-prims'
type big4k = big<_4Kbit>
// type big4K = bigint & supertype
`
#### big64
64-bit integer in the HighWidth (slow) range.
`ts`
export type big64 = big<_64bit>
> warning: may degrade performance!
>
> The Javascript platform only supports up to 54-bit integers with thenumber
> native type. Therefore, values of type big64 are unfortunatelybigint
> stored as , which is a variable-width format that supports intsnumber
> with hundreds or even thousands of bits, but is much slower than
> native . If it is possible to use int54 instead, prefer that.64
> Most other platforms have native support for -bit integers, but ifint54
> you want true cross-platform performance guarantees, stick to .
#### big96
96-bit integer in the HighWidth (slow) range.
`ts`
export type big96 = big<_96bit>
> warning: may degrade performance!
>
> Most platforms have no native support for 96-bit numbers. We emulatebigint
> them, in this case with and on other platforms in similar ways.
#### big128
128-bit integer in the HighWidth (slow) range.
`ts`
export type big128 = big<_128bit>
> warning: may degrade performance!
>
> Most platforms have no native support for 128-bit numbers. We emulatebigint
> them, in this case with and on other platforms in similar ways.
#### big160
160-bit integer in the HighWidth (slow) range.
`ts`
export type big160 = big<_160bit>
> warning: may degrade performance!
>
> Most platforms have no native support for 160-bit numbers. We emulatebigint
> them, in this case with and on other platforms in similar ways.
#### big192
192-bit integer in the HighWidth (slow) range.
`ts`
export type big192 = big<_192bit>
> warning: may degrade performance!
>
> Most platforms have no native support for 192-bit numbers. We emulatebigint
> them, in this case with and on other platforms in similar ways.
#### big256
256-bit integer in the HighWidth (slow) range.
`ts`
export type big256 = big<_256bit>
> warning: may degrade performance!
>
> Most platforms have no native support for 256-bit numbers. We emulatebigint
> them, in this case with and on other platforms in similar ways.
#### big512
512-bit integer in the HighWidth (slow) range.
`ts`
export type big512 = big<_512bit>
> warning: may degrade performance!
>
> Most platforms have no native support for 512-bit numbers. We emulatebigint
> them, in this case with and on other platforms in similar ways.
#### big4K
4096-bit integer in the HighWidth (slow) range.
`ts`
export type big4K = big<_4Kbit>
> warning: may degrade performance!
>
> Most platforms have no native support for 4096-bit numbers. We emulatebigint
> them, in this case with and on other platforms in similar ways.
#### _Lt
The union of all positive numbers from 0 up to,N
but not including, .
`ts`
export type _Lt
N extends A['length'] ? A[number] :
_Lt
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.
`ts`
export type Lt
Returns the union of all positive integers from 0 up to, but notN
including, . Safe version.
`ts`
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._Lt
This makes this type safe to use. Below the surface it is delegating to, 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.
`ts`
export type Lte
Returns the union of all positive integers from 0 up to andN
including, .
`ts``
type Lte7 = Lte<7>
// type Lte7 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
See Lt if you need an end-exclusive range.