caketype
Type-safe JSON validation? Runtime type-checking? Piece of cake.
Installation | Getting Started | Quick Reference | API Reference
Installation
Install the caketype package:
npm i caketype
caketype has only been thoroughly tested on TypeScript 4.9+, but it seems to work in 4.8, and 4.7 might still work too.
Getting Started
In TypeScript, types describe the structure of your data:
type Person = {
name: string;
age?: number;
};
const alice: Person = { name: "Alice" };
This helps us find type errors in our code:
const bob: Person = {};
However, TypeScript types are removed when your code is compiled. If you're working with parsed JSON or other unknown values at runtime, all bets are off:
const bob: Person = JSON.parse("{}");
This is a problem: the rest of our code assumes that bob is a Person, but at runtime, the name property is missing. In order for our code to be type-safe, we need to check whether the parsed JSON object matches the Person type at runtime.
Wouldn't it be great if we could write something like this instead?
const alice = Person.as(JSON.parse('{"name": "Alice"}'));
const bob = Person.as(JSON.parse("{}"));
Let's see how we can achieve this.
Introducing Cakes
A Cake is an object that represents a TypeScript type. Here's our Person type from earlier:
type Person = {
name: string;
age?: number;
};
The equivalent Cake looks like this:
import { bake, number, optional, string } from "caketype";
const Person = bake({
name: string,
age: optional(number),
});
Cakes are designed to resemble TypeScript types as much as possible, but there are still some differences:
- A Cake is an object, not a type, so we use
const Person instead of type Person. Giving the Cake the same name as the type is not required, but it makes imports more convenient, because importing Person in other files will import both the type and the Cake.
- The bake function takes an object that looks like a TypeScript type, and creates a Cake.
- Here, string and number refer to built-in Cakes, not types.
- To indicate that a property is optional, we use optional instead of
?.
Runtime Type-Checking with Cakes
Cake.as returns the value if it satisfies the Cake's type, and throws a TypeError otherwise:
const alice = Person.as({ name: "Alice" });
const bob = Person.as({});
Cake.is returns whether the value satisfies the Cake's type:
const alice = JSON.parse('{"name": "Alice"}');
if (Person.is(alice)) {
console.log(alice.name);
}
Lastly, Cake.check returns the validated value or a CakeError, wrapped in a Result:
const result = Person.check(JSON.parse('{"name": "Alice"}'));
if (result.ok) {
const alice = result.value;
console.log(alice.name);
} else {
console.error(result.error.toString());
}
Lenient Type-Checking
By default, runtime type-checking is stricter than TypeScript's static type-checking. For example, TypeScript allows objects to contain excess properties that are not declared in their type:
const carol = { name: "Carol", lovesCake: true };
const person: Person = carol;
However, the strict type-checking used by Cake.as, Cake.is, and Cake.check does not allow excess properties:
Person.as(carol);
To allow excess properties (and match TypeScript's static type-checking more closely in other ways), use the asShape, isShape, and checkShape methods instead:
Person.asShape(carol);
caketype is strict by default, under the assumption that developers should only opt-in to lenient type-checking when necessary. This has two benefits:
- Runtime type-checking is often used to validate untrusted parsed JSON values from network requests. Strict type-checking is typically more appropriate for this use case.
- It's easier to find and fix bugs caused by excessively strict type-checking, because values that should be okay will produce visible type errors instead. If the type-checking is too lenient, values that should produce type errors will be considered okay, which could have unexpected effects in other parts of your codebase.
Linking TypeScript Types to Cakes
If you have a TypeScript type and a corresponding Cake, you can link them by adding a type annotation to the Cake:
const Person: Cake<Person> = bake(...);
This ensures that the Cake always represents the specified type exactly. If you change the type without updating the Cake, or vice versa, you'll get a TypeScript type error:
type Person = {
name: string;
lovesCake: boolean;
};
const Person: Cake<Person> = bake({ name: string });
Inferring TypeScript Types from Cakes
If you want, you can also delete the existing definition of the Person type, and infer the Person type from its Cake:
import { Infer } from "caketype";
type Person = Infer<typeof Person>;
Creating Complex Cakes
More complex types, with nested objects and arrays, are also supported:
type Account = {
person: Person;
friends: string[];
settings: {
sendNotifications: boolean;
};
};
const Account = bake({
person: Person,
friends: array(string),
settings: {
sendNotifications: boolean,
},
});
- We can refer to the existing
Person Cake when defining the Account Cake.
- The array helper returns a Cake that represents arrays of the given type.
- Nested objects don't require any special syntax.
See the Quick Reference to create Cakes for even more types, like literal types and unions.
Quick Reference
| TypeScript Type | Cake |
|---|
|
Built-in named types:
any
boolean
bigint
never
number
string
symbol
unknown
|
Import the corresponding Cake:
import { number } from "caketype";
number.is(7);
See any, boolean, bigint, never, number, string, symbol, and unknown.
|
|
An object type:
type Person = {
name: string;
age?: number;
};
|
Use bake to create the corresponding Cake:
import { bake, number, optional, string } from "caketype";
const Person = bake({
name: string,
age: optional(number),
});
Person.is({ name: "Alice" });
|
|
An array:
type Numbers = number[];
|
Use array:
import { array, number } from "caketype";
const Numbers = array(number);
Numbers.is([2, 3]);
Numbers.is([]);
|
|
A union:
type NullableString = string | null;
|
Use union:
import { string, union } from "caketype";
const NullableString = union(string, null);
NullableString.is("hello");
NullableString.is(null);
|
|
A literal type:
true
7
"hello"
null
undefined
|
You can use literal values directly in most contexts. This Cake represents a union of string literals:
import { union } from "caketype";
const Color = union("red", "green", "blue");
And this Cake represents a discriminated union. Note the use of const assertions (as const), which are needed to infer the more specific literal types:
import { number, string, union } from "caketype";
const Operation = union(
{
operation: "get",
id: string,
} as const,
{
operation: "set",
id: string,
value: number,
} as const
);
Use bake if you actually need a Cake instance that represents a literal type:
import { bake } from "caketype";
const seven = bake(7);
seven.is(7);
seven.is(8);
|
API Reference
BAKING
bake
Create a Cake from a Bakeable type definition.
const Person = bake({
name: string,
age: optional(number),
});
Person.is({ name: "Alice" });
Use Infer to get the TypeScript type represented by a Cake:
type Person = Infer<typeof Person>;
const bob: Person = { name: "Bob", age: 42 };
Bakeable
A convenient syntax for type definitions. Used by bake.
type Bakeable = Cake | Primitive | ObjectBakeable;
type ObjectBakeable = {
[key: string | symbol]: Bakeable | OptionalTag<Bakeable>;
};
Baked
The return type of bake for a given Bakeable.
CAKES
Cake
Represent a TypeScript type at runtime.
abstract class Cake<in out T = any>
T: The TypeScript type represented by this Cake.
See bake to create a Cake.
Cake.as
Return the provided value if it satisfies the type represented by this
Cake, and throw a TypeError otherwise.
Cake<T>.as(value: unknown): T;
Using the built-in number Cake:
number.as(3);
number.as("oops");
The default type-checking behavior is stricter than TypeScript, making it
suitable for validating parsed JSON. For example, excess object properties
are not allowed.
See Cake.asShape for more lenient type-checking.
See Cake.check to return the error instead of throwing it.
Cake.asShape
Like Cake.as, but use lenient type-checking at runtime to match
TypeScript's static type-checking more closely.
Excess object properties are allowed with lenient type-checking,
but not allowed with strict type-checking:
const Person = bake({ name: string });
const alice = { name: "Alice", extra: "oops" };
Person.asShape(alice);
Person.as(alice);
Cake.check
Return a Result with the provided value if it satisfies the type
represented by this Cake, or a CakeError otherwise.
Cake<T>.check(value: unknown): Result<T, CakeError>;
Using the built-in number Cake:
function square(input: unknown) {
const result = number.check(input);
if (result.ok) {
return result.value ** 2;
} else {
console.error(result.error);
}
}
square(3);
square("oops");
Result.valueOr can be used to return a default value when
the provided value is invalid:
number.check(3).valueOr(0);
number.check("oops").valueOr(0);
Result.errorOr can be used to get the CakeError
directly, or a default value if no error occurred:
number.check(3).errorOr(null);
number.check("oops").errorOr(null);
The default type-checking behavior is stricter than TypeScript, making it
suitable for validating parsed JSON. For example, excess object properties
are not allowed.
See Cake.checkShape for more lenient type-checking.
See Cake.as to throw an error if the type is not satisfied.
Cake.checkShape
Like Cake.check, but use lenient type-checking at runtime to match
TypeScript's static type-checking more closely.
Excess object properties are allowed with lenient type-checking,
but not allowed with strict type-checking:
const Person = bake({ name: string });
const alice = { name: "Alice", extra: "oops" };
Person.checkShape(alice);
Person.check(alice);
Cake.is
Return whether a value satisfies the type represented by this Cake.
Cake<T>.is(value: unknown): value is T;
Using the built-in number Cake:
number.is(3);
number.is("oops");
This can be used as a
type guard
for control flow narrowing:
const value: unknown = 7;
if (number.is(value)) {
}
The default type-checking behavior is stricter than TypeScript, making it
suitable for validating parsed JSON. For example, excess object properties
are not allowed.
See Cake.isShape for more lenient type-checking.
Cake.isShape
Like Cake.is, but use lenient type-checking at runtime to match
TypeScript's static type-checking more closely.
Excess object properties are allowed with lenient type-checking,
but not allowed with strict type-checking:
const Person = bake({ name: string });
const alice = { name: "Alice", extra: "oops" };
Person.isShape(alice);
Person.is(alice);
Cake.toString
Return a human-readable string representation of the type represented by
this Cake.
const Person = bake({
name: string,
age: optional(number),
});
Person.toString();
The string representation is designed to be unambiguous and simple to
generate, so it may contain redundant parentheses.
The format of the return value may change between versions.
Infer
type
Get the TypeScript type represented by a Cake.
const Person = bake({
name: string,
age: optional(number),
});
type Person = Infer<typeof Person>;
BUILT-IN CAKES
any
A Cake representing the any type. Every value satisfies this type.
any.is("hello");
any.is(null);
See unknown to get the same runtime behavior, but an inferred type
of unknown instead of any.
array
Return a Cake representing an array of the specified type.
const nums = array(number);
nums.is([2, 3]);
nums.is([]);
nums.is(["oops"]);
nums.is({});
boolean
A Cake representing the boolean type.
boolean.is(true);
boolean.is(1);
bigint
A Cake representing the bigint type.
bigint.is(BigInt(5));
bigint.is(5);
integer
Like number, but only allow numbers with integer values.
integer.is(5);
integer.is(5.5);
Constraints are supported as well:
const NonNegativeInteger = integer.satisfying({ min: 0 });
NonNegativeInteger.is(5);
NonNegativeInteger.is(-1);
See number.satisfying.
never
A Cake representing the never type. No value satisfies this type.
never.is("hello");
never.is(undefined);
number
A Cake representing the number type.
number.is(5);
number.is("5");
Although typeof NaN === "number", this Cake does not accept NaN:
number.is(NaN);
number.as(NaN);
number.satisfying
Only allow numbers that satisfy the specified constraints.
const Percentage = number.satisfying({ min: 0, max: 100 });
Percentage.as(20.3);
Percentage.as(-1);
Percentage.as(101);
The min and max are inclusive:
Percentage.as(0);
Percentage.as(100);
Multiples of two:
const Even = number.satisfying({ step: 2 });
Even.as(-4);
Even.as(7);
Multiples of two, with an offset of one:
const Odd = number.satisfying({ step: 2, stepFrom: 1 });
Odd.as(7);
Odd.as(-4);
string
A Cake representing the string type.
string.is("hello");
string.is("");
string.satisfying
Only allow strings that satisfy the specified constraints.
const NonEmptyString = string.satisfying({ length: { min: 1 } });
NonEmptyString.as("hello");
NonEmptyString.as("");
Here, the length constraint is an object accepted by
number.satisfying; it can also be a number indicating the exact
length, or a Cake.
Strings matching a regular expression (use ^ and $ to match
the entire string):
const HexString = string.satisfying({ regex: /^[0-9a-f]+$/ });
HexString.as("123abc");
HexString.as("oops");
A Cake representing the string type.
string.is("hello");
string.is("");
symbol
A Cake representing the symbol type.
symbol.is(Symbol.iterator);
symbol.is(Symbol("hi"));
union
Return a Cake representing a union of the specified types.
Union members can be existing Cakes:
const StringOrNumber = union(string, number);
StringOrNumber.is("hello");
StringOrNumber.is(7);
StringOrNumber.is(false);
Union members can also be primitive values, or any other Bakeables:
const Color = union("red", "green", "blue");
type Color = Infer<typeof Color>;
Color.is("red");
Color.is("oops");
unknown
A Cake representing the unknown type. Every value satisfies this
type.
unknown.is("hello");
unknown.is(null);
See any to get the same runtime behavior, but an inferred type
of any instead of unknown.
TAGS
optional
Used to indicate that a property is optional.
const Person = bake({
name: string,
age: optional(number),
});
type Person = Infer<typeof Person>;
OptionalTag
Returned by optional.
CAKE ERRORS
CakeError
Represent the reason why a value did not satisfy the type represented by a
Cake.
CakeError.throw
Throw a TypeError created from this CakeError.
error.throw();
CakeError.toString
Return a human-readable string representation of this CakeError.
console.log(error.toString());
COMPARISON UTILITIES
sameValueZero
Return whether two values are equal, using the
SameValueZero
equality algorithm.
This has almost the same behavior as ===, except it considers NaN equal
to NaN.
sameValueZero(3, 3);
sameValueZero(0, false);
sameValueZero(0, -0);
sameValueZero(NaN, NaN);
MAP UTILITIES
Utility functions for manipulating
Maps,
WeakMaps,
and other MapLikes.
These functions can be imported directly, or accessed as properties of MapUtils:
const nestedMap: Map<number, Map<string, number>> = new Map();
import { deepSet } from "caketype";
deepSet(nestedMap, 3, "hi", 7);
import { MapUtils } from "caketype";
MapUtils.deepSet(nestedMap, 3, "hi", 7);
MapLike
Common interface for
Maps
and
WeakMaps.
interface MapLike<K, V> {
delete(key: K): boolean;
get(key: K): V | undefined;
has(key: K): boolean;
set(key: K, value: V): MapLike<K, V>;
}
deleteResult
Like
Map.delete,
but return an Ok with the value of the deleted entry, or an
Err if the entry does not exist.
const map: Map<number, string> = new Map();
map.set(3, "hi");
deleteResult(map, 3);
deleteResult(map, 3);
getResult
Like
Map.get,
but return an Ok with the retrieved value, or an Err if the
entry does not exist.
const map: Map<number, string> = new Map();
map.set(3, "hi");
getResult(map, 3);
getResult(map, 4);
getOrSet
If the map has an entry with the provided key, return the existing value.
Otherwise, insert an entry with the provided key and default value, and
return the inserted value.
const map: Map<number, string> = new Map();
map.set(3, "hi");
getOrSet(map, 3, "default");
getOrSet(map, 4, "default");
See getOrSetComputed to avoid constructing a default value if the
key is present.
getOrSetComputed
If the map has an entry with the provided key, return the existing value.
Otherwise, use the callback to compute a new value, insert an entry with the
provided key and computed value, and return the inserted value.
const map: Map<string, string[]> = new Map();
getOrSetComputed(map, "alice", () => []).push("bob");
getOrSetComputed(map, "alice", () => []).push("cindy");
The callback can use the map key to compute the new value:
const map: Map<string, string[]> = new Map();
getOrSetComputed(map, "alice", (name) => [name]).push("bob");
See getOrSet to use a constant instead of a computed value.
deepDelete
Like
Map.delete,
but use a sequence of keys to delete an entry from a nested map.
const map: Map<number, Map<string, number>> = new Map();
map.set(3, new Map());
map.get(3).set("hi", 7);
deepDelete(map, 3, "hi");
deepDeleteResult(map, 3, "hi");
deepDeleteResult
Like deepDelete, but return an Ok with the value of the
deleted entry, or Err if the entry does not exist.
const map: Map<number, Map<string, number>> = new Map();
map.set(3, new Map());
map.get(3).set("hi", 7);
deepDeleteResult(map, 3, "hi");
deepDeleteResult(map, 3, "hi");
deepGet
Like
Map.get,
but use a sequence of keys to get a value from a nested map.
const map: Map<number, Map<string, number>> = new Map();
map.set(3, new Map());
map.get(3).set("hi", 7);
deepGet(map, 3, "hi");
deepGet(map, 3, "oops");
deepGet(map, 4, "hi");
deepGetResult
Like deepGet, but return an Ok with the retrieved value, or
an Err if the entry does not exist.
const map: Map<number, Map<string, number>> = new Map();
map.set(3, new Map());
map.get(3).set("hi", 7);
deepGetResult(map, 3, "hi");
deepGetResult(map, 3, "oops");
deepGetResult(map, 4, "hi");
deepHas
Like
Map.has,
but use a sequence of keys to access a nested map.
const map: Map<number, Map<string, number>> = new Map();
map.set(3, new Map());
map.get(3).set("hi", 7);
deepHas(map, 3, "hi");
deepHas(map, 3, "oops");
deepHas(map, 4, "hi");
deepSet
Like
Map.set,
but use a sequence of keys to access a nested map.
const map: Map<number, Map<string, number>> = new Map();
deepSet(map, 3, "hi", 7);
deepSet(map, 4, new Map());
New nested
Maps
are constructed and inserted as necessary. Thus, all of the nested maps must
be Maps specifically.
If your nested maps are not Maps, you can use getOrSetComputed to
accomplish the same thing:
const map: WeakMap<object, WeakMap<object, number>> = new WeakMap();
const firstKey = {};
const secondKey = {};
const value = 5;
getOrSetComputed(map, firstKey, () => new WeakMap()).set(secondKey, value);
OBJECT UTILITIES
Utility functions for manipulating objects.
These functions can be imported directly, or accessed as properties of
ObjectUtils:
import { merge } from "caketype";
merge({ a: 1 }, { b: 2 });
import { ObjectUtils } from "caketype";
ObjectUtils.merge({ a: 1 }, { b: 2 });
Entry
Get the type of an object's entries.
type Person = { name: string; age: number };
type PersonEntry = Entry<Person>;
See entriesUnsound to get these entries from an object at runtime.
EntryIncludingSymbols
Get the type of an object's entries, and include entries with symbol keys
too.
const sym = Symbol("my symbol");
type Example = { age: number; [sym]: boolean };
type ExampleEntry = EntryIncludingSymbols<Example>;
See entriesIncludingSymbolsUnsound to get these entries from an
object at runtime.
entries
Alias for
Object.entries
with a sound type signature.
See entriesUnsound to infer a more specific type for the entries
when all of the object's properties are declared in its type.
See this issue
explaining why Object.entries is unsound.
entriesUnsound
Like
Object.entries,
but use the object type to infer the type of the entries.
This is unsound unless all of the object's own enumerable string-keyed
properties are declared in its type. If a property is not declared in the
object type, its entry will not appear in the return type, but the entry will
still be returned at runtime.
If a property is not enumerable, its entry will not be returned at runtime,
even if its entry appears in the return type.
entriesIncludingSymbols
Return the entries of an object's own enumerable properties.
This differs from
Object.entries
by including properties with symbol keys.
See entriesIncludingSymbolsUnsound to infer a more specific type
for the entries when all of the object's properties are declared in its type.
entriesIncludingSymbolsUnsound
Like entriesIncludingSymbols, but use the object type to infer
the type of the entries.
This is unsound unless all of the object's own enumerable properties are
declared in its type. If a property is not declared in the object type, its
entry will not appear in the return type, but the entry will still be
returned at runtime.
If a property is not enumerable, its entry will not be returned at runtime,
even if its entry appears in the return type.
keys
Alias for
Object.keys.
See keysUnsound to infer a more specific type for the keys when all
of the object's properties are declared in its type.
keysUnsound
Like
Object.keys,
but use the object type to infer the type of the
keys.
This is unsound unless all of the object's own enumerable string-keyed
properties are declared in its type. If a property is not declared in the
object type, its key will not appear in the return type, but the key will
still be returned at runtime.
If a property is not enumerable, its key will not be returned at runtime,
even if its key appears in the return type.
keysIncludingSymbols
Return the string and symbol keys of an object's own enumerable properties.
This differs from
Object.keys
by including symbol keys, and it differs from
Reflect.ownKeys
by excluding the keys of non-enumerable properties.
See keysIncludingSymbolsUnsound to infer a more specific type for
the keys when all of the object's properties are declared in its type.
keysIncludingSymbolsUnsound
Like keysIncludingSymbols, but use the object type to infer the type
of the keys.
This is unsound unless all of the object's own enumerable properties are
declared in its type. If a property is not declared in the object type, its
key will not appear in the return type, but the key will still be returned at
runtime.
If a property is not enumerable, its key will not be returned at runtime,
even if its key appears in the return type.
values
Alias for
Object.values
with a sound type signature.
See valuesUnsound to infer a more specific type for the values when
all of the object's properties are declared in its type.
See this issue
explaining why
Object.values
is unsound.
valuesUnsound
Like
Object.values,
but use the object type to infer the type of the values.
This is unsound unless all of the object's own enumerable string-keyed
properties are declared in its type. If a property is not declared in the
object type, its value will not appear in the return type, but the value will
still be returned at runtime.
If a property is not enumerable, its value will not be returned at runtime,
even if its value appears in the return type.
valuesIncludingSymbols
Return the values of an object's own enumerable properties.
This differs from
Object.values
by including properties with symbol keys.
valuesIncludingSymbolsUnsound
Like valuesIncludingSymbols, but use the object type to infer the
type of the values.
This is unsound unless all of the object's own enumerable properties are
declared in its type. If a property is not declared in the object type, its
value will not appear in the return type, but the value will still be
returned at runtime.
If a property is not enumerable, its value will not be returned at runtime,
even if its value appears in the return type.
mapValues
Return a new object created by mapping the enumerable own property values of
an existing object. This is analogous to
Array.map.
See mapValuesUnsound to infer more specific types for the keys and
values when all of the object's properties are declared in its type.
mapValuesUnsound
Like mapValues, but use the object type to infer the types of the
values and keys.
This is unsound unless all of the object's own enumerable properties are
declared in its type. If a property is not declared in the object type, its
key and value cannot be type-checked against the function parameter types,
but the function will still be called with that key and value at runtime.
lookup
Find the first object with the given key set to a non-undefined value, and
return that value. If none of the objects have a non-undefined value,
return undefined.
lookup("a", { a: 1 }, { a: 2 });
lookup("a", { a: undefined }, { a: 2 });
lookup("a", {}, { a: 2 });
lookup("a", {}, {});
If the objects contain properties that are not declared in their
types, the inferred return type could be incorrect. This is because an
undeclared property can take precedence over a declared property on a later
object:
const aNumber = { value: 3 };
const aString = { value: "hi" };
const propertyNotDeclared: {} = aString;
const wrong: number = lookup("value", propertyNotDeclared, aNumber);
merge
Return a new object created by merging the enumerable own properties of the
provided objects, skipping properties that are explicitly set to undefined.
merge({ a: 1, b: 2, c: 3 }, { a: 99, b: undefined });
If the objects contain properties that are not declared in their
types, the inferred type of the merged object could be incorrect. This is
because an undeclared property can replace a declared property from a
preceding object:
const aNumber = { value: 3 };
const aString = { value: "hi" };
const propertyNotDeclared: {} = aString;
const wrong: { value: number } = merge(aNumber, propertyNotDeclared);
Remarks
By default, TypeScript allows optional properties to be explicitly set to
undefined. When merging partial objects, it's often desirable to skip
properties that are set to undefined in order to use a default value:
const defaults = { muted: false, volume: 20 };
const muted = merge(defaults, { muted: true, volume: undefined });
Object.assign
and
object spreading
do not give the desired result:
const wrong1 = { ...defaults, ...{ muted: true, volume: undefined } };
If the TypeScript flag
exactOptionalPropertyTypes
is not enabled, then undefined can be assigned to any optional property,
and object spreading is unsound:
type Config = { muted: boolean; volume: number };
const overrides: Partial<Config> = { muted: true, volume: undefined };
const wrong2: Config = { ...defaults, ...overrides };
const volume: number = wrong2.volume;
omit
Return a new object created by copying the enumerable own properties of a
source object, but omitting the properties with the specified keys.
See omitLoose to allow keys that are not keyof T.
omitLoose
Return a new object created by copying the enumerable own properties of a
source object, but omitting the properties with the specified keys.
The type of the returned object will be inaccurate if any of the keys are not
literal strings or unique symbols. For example, providing a key with type
string will omit all string-keyed properties from the type of the returned
object. This type inference limitation does not affect the runtime behavior.
See omit to restrict the keys to keyof T.
pick
Return a new object created by copying the properties of a source object with
the specified keys.
Non-enumerable properties are copied from the source object, but the
properties will be enumerable in the returned object.
PRIMITIVES
Primitive
A JavaScript
primitive value.
type Primitive = bigint | boolean | number | string | symbol | null | undefined;
isPrimitive
Return whether a value is a Primitive.
isPrimitive(5);
isPrimitive([]);
stringifyPrimitive
Return a string representation of a Primitive that resembles how it
would be written as a literal in source code.
console.log(stringifyPrimitive(BigInt(-27)));
console.log(stringifyPrimitive("hi\nbye"));
console.log(stringifyPrimitive(Symbol("apple")));
RESULTS
Result
Represent the result of an operation that could succeed or fail.
type Result<T, E> = Ok<T> | Err<E>;
This is a
discriminated union,
where Ok represents a successful result, and Err represents
an unsuccessful result.
Using an existing Result:
function showDecimal(input: string): string {
const result: Result<number, string> = parseBinary(input);
if (result.ok) {
return `binary ${input} is decimal ${result.value}`;
} else {
return `not a binary number: ${result.error}`;
}
}
showDecimal("101");
showDecimal("foo");
showDecimal("");
Creating Results:
function parseBinary(input: string): Result<number, string> {
if (input.length === 0) {
return Result.err("empty string");
}
let num = 0;
for (const char of input) {
if (char === "0") {
num = num * 2;
} else if (char === "1") {
num = num * 2 + 1;
} else {
return Result.err("invalid character");
}
}
return Result.ok(num);
}
parseBinary("101");
parseBinary("foo");
parseBinary("");
Result.ok
function
Return an Ok with the provided value, using undefined if no value is
provided.
Result.ok(5);
Result.ok();
Result.err
function
Return an Err with the provided error, using undefined if no error is
provided.
Result.err("oops");
Result.err();
Result.valueOr
method
Return Ok.value, or the provided argument if this is not an Ok.
Result.ok(5).valueOr(0);
Result.err("oops").valueOr(0);
Result.errorOr
method
Return Err.error, or the provided argument if this is not an Err.
Result.err("oops").errorOr("no error");
Result.ok(5).errorOr("no error");
Result.toString
method
Return a string representation of this Result.
Result.ok(5).toString();
Result.err({}).toString();
Ok
class
The result of a successful operation.
See Result.ok to construct an Ok.
Ok.ok
property
Always true. In contrast, Err.ok is always false.
Ok.value
property
The value returned by the successful operation.
Err
class
The result of an unsuccessful operation.
See Result.err to construct an Err.
Err.ok
property
Always false. In contrast, Ok.ok is always true.
Err.error
property
The value returned by the unsuccessful operation.
TYPE-LEVEL ASSERTIONS
Inspect and assert relationships between types. For example, you can
assert that one type extends another, or that two types are equivalent.
This can be used to test complex conditional types and type inference.
Assert
Assert that the type argument is always true, or cause a type error
otherwise.
type Assert<T extends true> = never;
Typically used with Equivalent, or another generic type that returns
a boolean.
type _pass = Assert<Equivalent<string["length"], number>>;
type _fail = Assert<Equivalent<string, number>>;
See AssertExtends to get more specific error messages if you are
asserting that one type extends another.
AssertExtends
Assert that T extends U.
type AssertExtends<T extends U, U> = never;
Example
type _pass = AssertExtends<3, number>;
type _fail = AssertExtends<number, 3>;
This behaves like Assert combined with Extends, but it uses
generic constraints so you can get more specific error messages from the
TypeScript compiler.
Equivalent
If T and U extend each other, return true. Otherwise, return false.
type Equivalent<T, U> = [T] extends [U]
? [U] extends [T]
? true
: false
: false;
Example
type _true = Equivalent<string["length"], number>;
type _false = Equivalent<3, number>;
Extends
If T extends U, return true. Otherwise, return false.
type Extends<T, U> = [T] extends [U] ? true : false;
Example
type _ = Assert<Not<Extends<number, 3>>>;
See AssertExtends to get more specific error messages if you are
asserting that one type extends another.
If
If T is true, return U. Otherwise, return V.
type If<T extends boolean, U, V> = T extends true ? U : V;
Example
type _apple = If<true, "apple", "banana">;
type _either = If<true | false, "apple", "banana">;
Not
Return the boolean negation of the type argument.
type Not<T extends boolean> = T extends true ? false : true;
Example
type _false = Not<true>;
type _boolean = Not<boolean>;
UTILITY TYPES
Class
The type of a class (something that can be called with new to construct
an instance).
interface Class<T = any, A extends unknown[] = any>
T - Type of the class instances.
A - Type of the constructor arguments.
Example
const a: Class[] = [Date, Array, RegExp];
const b: Class<Date> = Date;
const c: Class<Date, [number]> = Date;
Changelog
v0.5.0 - 2023-02-10
Added
Changed
- number no longer accepts
NaN (#64)
- Built-in named Cakes (e.g. boolean) now have type
Cake instead of TypeGuardCake (#60)
- Replaced
TypeGuardFailedCakeError with WrongTypeCakeError, which is more general and has a more concise error message (#60)
v0.4.1 - 2023-01-26
Added
Changed
Cake<...> type annotations now enforce type equivalence (#56)
v0.4.0 - 2023-01-22
Added
Changed
Removed
- Cake.asStrict, Cake.checkStrict, and Cake.isStrict, since strict checking is the default behavior now (#50)
v0.3.0 - 2023-01-19
Added
v0.2.0 - 2023-01-11
Added