Skip to main content

Types

The type system is the foundation everything in Lo is built on. This page covers how to use it directly: getting a value's type, checking capabilities, converting between types, and extending the registry. For reasoning behind the design, check out the about page.

Getting the type of a value

getType returns a cached type descriptor for any value

import { getType, TYPES } from 'lo';

getType([]); // TYPES.Array
getType(new Set()); // TYPES.Set
getType(42); // TYPES.Number
getType(null); // TYPES.Null
getType(undefined); // TYPES.Undefined

Descriptors are singletons, so you can compare them by identity

getType(value) === TYPES.Array;

getType is safe. It always returns a descriptor and never undefined, falling back to TYPES.Unknown for unrecognized values, so accessors never need a guard

getType(anything).collection; // never throws

Passing a descriptor back into getType returns it unchanged, so the function is idempotent

getType(TYPES.Set) === TYPES.Set;

If you just want the name as a string, you can use getTypeStr (which falls back to the value's toStringTag for unknown types)

import { getTypeStr } from 'lo';

getTypeStr(new Set()); // 'Set'
getTypeStr(42); // 'Number'

Type descriptors

Each descriptor includes both a type's identity and its capabilities

FieldTypeDescription
typestringtypeof category (object, function, string, ...)
namestringType name (Set, Uint8Array, Null, ...)
ctorfunctionConstructor
protoobjectPrototype
constructbooleanWhether it can be constructed
callbooleanWhether it can be called
createnumberCreation strategy: 0=none, 1=call, 2=construct
collectionbooleanTreated as a first-class iterable collection
eachbooleanHas forEach
iterablebooleanHas @@iterator
asyncbooleanHas @@asyncIterator
entriesbooleanHas entries()
objectbooleanIs a non-null object type
knownbooleanExists in the current runtime

The each, iterable, async, and entries flags are derived from the prototype automatically, so they always match reality

Checking types

Predicate helpers wrap getType for common checks and read naturally

import { isArray, isString, isPromise, isError } from 'lo';

isArray([]); // true
isString('hi'); // true
isPromise(fetch('/')); // true
isError(new TypeError()); // true
CategoryPredicates
NilisNil, notNil, isNull, isUndefined
PrimitivesisNumber, isNumeric, isString, isBoolean
ObjectsisObject, isModule, isRegExp, isError
FunctionsisFunction, isAsyncFunction, isGeneratorFunction
IterationisIterable, isAsyncIterable, isCollection, hasForEach, hasEntries
BinaryisTypedArray, isArrayBuffer
MetaisCtor, isClass, isIterator, isPromise, isArray

Iteration and collection types

Iteration helpers are how Lo supports every iterable shape (arrays, maps, sets, typed arrays, iterators, generators, and async iterables), through one consistent surface

import { isCollection, isIterable, isAsyncIterable, hasForEach, hasEntries } from 'lo';

isCollection(new Set()); // true (a first-class collection)
isIterable('abc'); // true (has @@iterator)
isAsyncIterable(stream); // true (supports for await...of)
hasForEach(new Uint8Array(4)); // true
hasEntries(new Map()); // true

A collection type is iterated element-by-element. Anything that is not a collection type is treated as a single-item collection, so operations behave predictably on scalars and plain objects alike. Collection types include Array, Map, Set, the *Iterator types, Generator / AsyncGenerator, TypedArray, Buffer, and platform collections like NodeList and Headers.

The distinction between iterable and collection is deliberate: a String is iterable but is not a collection, so it's treated as one value rather than a stream of characters unless you ask otherwise.

Types from constructors

getCtorType resolves a constructor to the type of the values it produces (the inverse of getType)

import { getCtorType, TYPES } from 'lo';

getCtorType(Promise); // TYPES.Promise
getCtorType(Boolean); // TYPES.Boolean

getCtorTypeStr returns the name as a string

Casting and empty values

toType casts a value to a target type using that type's creation strategy

import { toType } from 'lo';

toType(Set, [1, 2, 3]); // Set(3) { 1, 2, 3 }
toType(Boolean, 1); // true
toType(Map, [[1, 2]]); // Map(1) { 1 => 2 }

of returns an empty value of the same type (useful for accumulators)

import { of } from 'lo';

of([1, 2, 3]); // []
of(new Set([1, 2])); // Set(0) {}
of({ a: 1 }); // {}
of(10); // 0
of('text'); // ''

Converting values

import { toArray, toObject, toString } from 'lo';

toArray(new Set([1, 2, 3])); // [1, 2, 3]
toArray('123'); // ['1', '2', '3']
toArray(null); // []
toObject(new Map([['a', 1]])); // { a: 1 }
toString([1, 2, 3]); // '1,2,3'

These conversions are driven by the same descriptors, so they work across every registered type (including iterables and iterator objects), rather than a hard-coded list.

The type registry

All known types live in the TYPES constant (keyed by name)

import { TYPES } from 'lo';

TYPES.Map.collection; // true
TYPES.String.iterable; // true

The table is split into tree-shakeable logical groups. Each entry point registers groups relevant to its target. You can register additional types (including optional groups the library ships, but doesn't load by default) with addTypes

import { addTypes } from 'lo';
import { io } from 'lo/types/io';

addTypes(io); // add server/IO handle types (http, net, dgram, child_process)

addType registers a single type record the same way

Unknown types

By default, an unregistered subclass resolves to its nearest registered ancestor (with the correct derived capability flags). A custom readable stream subclass resolves to Readable, and a DOM node subtype resolves to Node. To cache unknown types in the registry using their own identity instead, you can enable the addUnknownTypes setting

import { settings } from 'lo/constants';

settings.addUnknownTypes = true;

This trades a bounded, predictable type table for more specific naming of user-defined types. See the about page for tradeoffs.