@hyurl/schematize v1.1.1
Schematize
Utilities to ensure object structures and perform pattern matching.
For many reasons, an API may not return the data to what we want them to be. For
example, we expect a JSON response should have a foo
property of number
type,
however, the server provides it as a numeric string
, or even worse it
doesn't exist at all, or is set null
, which would cause the client to crash
if the exception is not handled well.
That's why schematize comes on stage. It ensures the input or output data must be of a certain structure based on the schema, which provides the ability to auto-cast compatible values and provides default values when they're missing.
Install
npm i @hyurl/schematize
Example
schematize
import schematize, { Optional } from "@hyurl/schematize";
import * as express from "express";
let AuthorSchema = {
uid: Number,
username: String,
birthday: Optional(Date), // use 'Optional' to define an optional property
isPopular: Boolean,
contactInfo: { // child schema is supported and unlimited
mobile: String,
email: {
personal: String,
work: String
}
}
};
// Client-side example
(async () => {
let res = await fetch("https://localhost/author/12345");
if (res.ok) {
// 'author' will be well-typed in TypeScript.
let author = schematize(await res.json(), authorSchema);
console.log(
`${author.username} (${author.uid}) ${author.isPopular ? "is" : "isn't"} a popular writer.`);
if (author.birthday) { // 'birthday' is optional, it could be missing
console.log(`His birthday is ${author.birthday}.`);
}
}
})();
// Server-side example
(async () => {
let app = express();
app.get("/author/:uid", (req, res) => {
// assume db is defined
let author = await db.findOne({ uid: Number(req.params.uid) });
if (author) {
// At this point, we don't know what fields does the 'author' has,
// but that's no problem, `schematize()` will make sure that all the
// fields we expected are presented in the outgoing response.
res.send(schematize(author, AuthorSchema));
}
});
app.listen(80);
})();
match
import { match, Optional } from "@hyurl/schematize";
let userSchema = { uid: Number, username: String, birthday: Optional(String) };
let res = await fetch(someLink);
if (res.ok) {
let data = await res.json();
// Assume the structure of the returning data from fetch(someLink) could be
// in multiple forms, we could use `match()` to do pattern-matching.
if (match(data, userSchema)) {
// 'data' will be well-typed in TypeScript in the conditional block
console.log(`${data.username} (${data.uid}), born on: ${data.birthday || "unknown"}`);
} else if (match(data, { code: Number, data: userSchema })) {
let _data = data.data;
console.log(`${_data.username} (${_data.uid}), born on: ${_data.birthday || "unknown"}`);
} else if (match(data, { code: Number, reason: String })) {
console.log(`Failed to fetch user: ${data.reason} (${data.code})`);
}
}
API
schematize
/**
* Ensures the input object is restraint with the types defined in the schema
* and automatically fills any property that is missing.
* @param omitUntyped If set, those properties that are not specified in the
* schema will be removed.
*/
function schematize<T>(
obj: any,
schema: T,
omitUntyped?: boolean
): Structured<T>;
/**
* Ensures the input array of objects is restraint with the types defined in
* the schema and automatically fills any property that is missing.
* @param schema For array of objects, the schema must be defined as an array
* with one element which sets the types for all objects in the input array.
* @param omitUntyped If set, those properties that are not specified in the
* schema will be removed.
*/
function schematize<T>(
arr: any[],
schema: [T],
omitUntyped?: boolean
): Structured<T>[];
match
/**
* Performs a pattern matching on an object according to the given schema.
* @param exactMatch If set, the object must only contain the properties that
* are presented in the schema.
*/
function match<T extends object>(
obj: object,
schema: T,
exactMatch?: boolean
): obj is Structured<T>;
/**
* Performs a pattern matching on an array according to the given schema.
* @param schema For array of objects, the schema must be defined as an array
* with one element which sets the types for all objects in the input array.
* @param exactMatch If set, the objects in the input array must only contain
* the properties that are presented in the schema.
*/
export default function match<T>(
arr: any[],
schema: [T],
exactMatch?: boolean
): arr is Structured<T>[];
For more details about types, please check the type definition.
More On Schematize
Default Values
For the schematize()
function, by default, if you provide a type constructor
in the schema, when the specified property is missing, it will create a default
value to make sure the property always available (expect using
Optional
wrapper). The default values of each types are:
String
=>''
(empty string)Number
=>0
BigInt
=>0n
Boolean
=>false
Object
=>{}
(empty plain object for plain object constructor)Array
=>[]
Date
=>new Date()
(The current date)Map
=>new Map([])
(empty map)Set
=>new Set([])
(empty set)Buffer
=>Buffer.from([])
(empty buffer)
Other type constructors (include Symbol
and user-defined classes) are all set
null
, if the relevant property is missing.
Other than these, you can always by providing an instance value to the schema, and it will be used as the default value of the property automatically.
(NOTE: you can also set an instance value in the schema of match()
function,
but it will be used for full-equality comparison.)
Auto-cast Types
When a property in the input data of the schematize()
function isn't of the
type that defined in the schema, however, it is compatible to the type, or can
generate a similar representation, the value will be automatically casted into
an instance of the defined type.
For example:
let schema = { url: URL }; // 'url' is defined as a URL
let data = schematize({
url: "https://example.com" // but provided a string
}, schema);
assert(data.url instanceof URL); // 'data.url' will be auto-casted to URL
The current casting rule is as the following:
String
Date
to ISO stringObject
orArray
to JSON stringSymbol
in global registry, to string- any other types that are not functions or classes can be casted to strings
Number
BigInt
to its number equivalentsString
that is numericDate
to Unix timestamp in millisecondstrue
to1
false
to0
BigInt
Number
to its bigint equivalents- other types are similar to
Number
Boolean
Number
/BigInt
:0
/0n
tofalse
, others to1
String
/^([Tt]rue|[Yy]es|[Oo]n|1)$/
casted totrue
/^([Ff]alse|[Nn]o|[Oo]ff|0)$/
casted tofalse
Symbol
String
matches/Symbol\(.*?\)/
or not, casted to global-scoped symbol
Object
Object
derivatives, create a shallow copyString
starts with{
and ends with}
will try to be parsed as JSON
Array
String
- starts with
[
and ends with]
will try to be parsed as JSON - others will be split by
,
- starts with
Date
Number
used as Unix timestampString
used as date-time string
URL
String
a valid URL string
RegExp
String
a valid RegExp literal
Map
Array
a valid Map entry
Set
Array
Buffer
Uint8Array
ArrayBuffer
Array
that only contains numbers ranged from0
-255
TypedArray
- any iterable object with numbers will try to be casted
Error
String
used as error messageObject
with signature{ name: string, message: string, stack?: string }
(NOTE: match()
function doesn't support auto-casting.)