Built In Types
Record
Use ft.Record to validate an unknown value is a "Record object", i.e. an object where the keys match some expected pattern, and the values are of some known type.
Simple Record
import * as ft from "funtypes";
export const MyRecordCodec = ft.Record(
ft.String,
ft.Number,
);
// => ft.Codec<{ [key in string]?: number }>
export type MyRecord = ft.Static<
typeof MyRecordCodec
>;
// => { [key in string]?: number }
// β
Valid Record
assert.deepEqual(
MyRecordCodec.parse({ "Forty Two": 42 }),
{ "Forty Two": 42 },
);
// β
Valid Empty Record
assert.deepEqual(MyRecordCodec.parse({}), {});
// π¨ Value doesn't match `ft.Number` codec
assert.throws(() =>
MyRecordCodec.parse({ "42": "Forty Two" }),
);
// π¨ Array rather than object
assert.throws(() => MyRecordCodec.parse([]));
Complex Keys
Record keys can be any Codec type with an underlying type of string or number. Values can be any Codec type.
import * as ft from "funtypes";
type Email = `${string}@${string}`;
const EmailCodec =
ft.String.withConstraint<Email>(
(value) =>
value.includes("@") ||
"Expected a valid email",
{ name: "Email" },
);
const UserCodec = ft.Object({
id: ft.Number,
name: ft.String,
});
export const UsersByEmailCodec = ft.Record(
EmailCodec,
UserCodec,
);
// => ft.Codec<{ [key in Email]?: { id: number; name: string } }>
export type UsersByEmail = ft.Static<
typeof UsersByEmailCodec
>;
// => { [key in Email]?: { id: number; name: string } }
// β
Valid Record
assert.deepEqual(
MyRecordCodec.parse({
"forbes@example.com": {
id: 42,
name: "Forbes Lindesay",
},
}),
{
"forbes@example.com": {
id: 42,
name: "Forbes Lindesay",
},
},
);
// β
Valid Empty Record
assert.deepEqual(MyRecordCodec.parse({}), {});
// π¨ Invalid Key
assert.throws(() =>
MyRecordCodec.parse({
forbes: {
id: 42,
name: "Forbes Lindesay",
},
}),
);
Numbers as Keys
import * as ft from "funtypes";
export const MyRecordCodec = ft.Record(
ft.Number,
ft.String,
);
// => ft.Codec<{ [key in number]?: string }>
export type MyRecord = ft.Static<
typeof MyRecordCodec
>;
// => { [key in number]?: string }
// β
Valid Record
assert.deepEqual(
MyRecordCodec.parse({ 42: "Forty Two" }),
{ 42: "Forty Two" },
);
// β
Valid Record (runtime value is the same)
assert.deepEqual(
MyRecordCodec.parse({ "42": "Forty Two" }),
{ "42": "Forty Two" },
);
// β
Valid Empty Record
assert.deepEqual(MyRecordCodec.parse({}), {});
// π¨ Key doesn't match `ft.Number` codec
assert.throws(() =>
MyRecordCodec.parse({
"Forty Two": "Forty Two",
}),
);
ReadonlyRecord
The ft.ReadonlyRecord type has the same runtime behaviour as ft.Record, but the type is a readonly object, rather than a mutable object.
import * as ft from "funtypes";
export const MyRecordCodec = ft.ReadonlyRecord(
ft.String,
ft.Number,
);
// => ft.Codec<{ readonly [key in string]?: number }>
export type MyRecord = ft.Static<
typeof MyRecordCodec
>;
// => { readonly [key in string]?: number }