Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (type-predicate-generator) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
TypeScript Type Predicate Generator
A.k.a type guard generator.
About
A TypeScript type predicate generator that produces strictly type safe readable and extremely fast TypeScript code.
Yep, the type predicates it generates are themselves strictly type checked by TS that guarantees that the checked value satisfies the expected type.
Status
Alpha. Most of the key distinctive features are proven to work, but some essential features are still be missing (see Known Limitations).
Run
nvm install 22
npm i
npm run --silent generate -- "./example.ts" > "example.guard.ts"
Why
It's a simple, easy to integrate tool that does only one thing and does it well: generates type safe and fast code ready to use right away. The implmentation is near trivial, it uses minimal TypeScript public API surface thus is easy to update to keep up with changes in TS itself.
Experience shows that many teams can remain hesitant to introduce a runtime type checker because of many reasons. The main two have been speed (some checkers bring a whole runtime engine with them) and reliability (the produced code is invisible and hard to fix).
To account for the above this generator emits explicitly readable code that is easy to audit and support. The produced code is as fast as if manually written and minifies really well. This all is heavily inspired by code generators from other languages.
Pros
- The produced code is type safe and gets checked by your TS setup
- The produced code is as fast as it gets: no extra reads, calls, comparisons
- The produced code is readable, lenear and easy to modify in case you need to
- Does not require any runtime or compile time dependencies
- It's bundler agnostic as it's output is plain TS (no
tsc
plugins required) - The bundle size cost is 100% visible and predictable
- Safe to upgrade: if the produced code changes you'll see it in the PR
- Zero performance cost in development: run once and forget
- Full IDE support: jump to definition just works
- Cannot unexpectedly break as the produced code is static and checked into your repository
- Reliable: the tool rejects the types it cannot cover 100%
- Easy to debug and fix: the stacktrace points exactly at where the bug is
- No vendor lock-in: any tool that works with TS can be used instead
- Unix-way: relies on other tools for minification, dead code elimination, etc
Cons
These are by desing, fixing them would affect the Pros:
- Compared to
tsc
plugins it requires a separate build step - Compared to
tsc
plugins it reads a file and produces a file
See Known Limitations for more on low level missing bits.
Example
The file with the types:
// example.ts
export type User = {
id: number;
login: string;
bio: {
first: string;
last: string;
};
};
export type Post = {
title: string;
text: string;
link?: string;
published: boolean;
author: User;
list: Array<number | string>;
};
Running the generator on it:
npm run --silent generate -- "./example.ts" > "example.guard.ts"
This is the output with a readable and strictly type safe TS guard:
// example.guard.ts
import { type User, type Post } from "./example.ts";
type SafeShallowShape<Type> = {
[_ in keyof Type]?: unknown;
};
const safeIsArray: (v: unknown) => v is unknown[] = Array.isArray;
function ensureType<T>(_: T) {}
export function isUser(root: unknown): root is User {
if (!(typeof root === "object" && root !== null)) {
return false;
}
const { id, login, bio }: SafeShallowShape<User> = root;
if (!(typeof id === "number")) {
return false;
}
if (!(typeof login === "string")) {
return false;
}
if (!(typeof bio === "object" && bio !== null)) {
return false;
}
const { first, last }: SafeShallowShape<User["bio"]> = bio;
if (!(typeof first === "string")) {
return false;
}
if (!(typeof last === "string")) {
return false;
}
ensureType<User>({
id,
login,
bio: {
first,
last,
},
});
return true;
}
export function isPost(root: unknown): root is Post {
type Element = Post["list"][number];
function isElement(root: unknown): root is Element {
if (!(typeof root === "string" || typeof root === "number")) {
return false;
}
ensureType<Element>(root);
return true;
}
if (!(typeof root === "object" && root !== null)) {
return false;
}
const {
title,
text,
link,
published,
author,
list,
}: SafeShallowShape<Post> = root;
if (!(typeof title === "string")) {
return false;
}
if (!(typeof text === "string")) {
return false;
}
if (!(link === undefined || typeof link === "string")) {
return false;
}
if (!(typeof published === "boolean")) {
return false;
}
if (!isUser(author)) {
return false;
}
if (!(safeIsArray(list) && list.every(isElement))) {
return false;
}
ensureType<Post>({
title,
text,
link,
published,
author,
list,
});
return true;
}
And this is what esbuild
minifies it into (formatted for readability):
// example.guard.min.js
const u = Array.isArray;
function p(e) {}
export function isUser(e) {
if (!(typeof e == "object" && e !== null)) return !1;
const { id: s, login: r, bio: t } = e;
if (
typeof s != "number" ||
typeof r != "string" ||
!(typeof t == "object" && t !== null)
)
return !1;
const { first: n, last: f } = t;
return typeof n != "string" || typeof f != "string" ? !1 : !0;
}
export function isPost(e) {
function s(o) {
return typeof o == "string" || typeof o == "number" ? !0 : !1;
}
if (!(typeof e == "object" && e !== null)) return !1;
const {
title: r,
text: t,
link: n,
published: f,
author: l,
list: i,
} = e;
return typeof r != "string" ||
typeof t != "string" ||
!(n === void 0 || typeof n == "string") ||
typeof f != "boolean" ||
!isUser(l) ||
!(u(i) && i.every(s))
? !1
: !0;
}
As you can see, esbuild nicely merges all the if
s for the same set of properties into just one combined check.
Known Limitations
Most of the below is gonna be eventually fixed.
No support for extended schema verification. This is mostly to stay simple and fast to evolve while in alpha/beta. It's trivial to add more value checkers with the current design.
Does not produce error messages yet. As the errors happen really rarely in production the plan is to generate the error reporters separately and load them on demand. Error reporters are usually more versitile and don't minify that well as the code has to carry the context around and check produce a custom message for every property. The current workaround is to either simply stringify the falsy value or load a third party runtime schema chacker on error.
No support for generics atm, but the code is designed with them in mind, so also coming soon.
Anonymous object types in unions are not supported.
// instead of
type X = {
union: { a: string } | { b: number };
};
// use
type A = { a: string };
type B = { b: number };
type X = {
union: A | B;
};
See more here #1.
Expects
strict: true
, otherwise every type is nullable which defends the purpose.Avoid trivial aliases like
type X = Y
as TypeScript erases the information about thatX
is an alias toY
and they effectively become the same type. This produces duplicate code forX
where it would be just a shared predicate function likeconst isX = isY
orfunction isX(…) { return isY() }
. It is possible to fix by considering AST nodes in addition to symbols and type objects, but it's not a common use case, so for now not handled properly.
Prior art
- Inspiring ts-auto-guard
- Groundbreaking ts-runtime-checks
- Impressive typia
Contributing
TODO
- Support lists
- Support tuples
- Implement installing as a CLI
- Implement a dynamic demo (1)
- Generate unit tests with example data
Architecture
This tool is simple if not trivial. The code generator uses the TypeScript public API to emit the valid TS code. The type parser uses the TypeScript public API too to walk the type graph.
What this tool does in its own way is using an intermediate type representation that interfaces the generator with the type parser (see TypeModel
type). The parser produces a model object that has no trace of the ts.*
structures in it. This model object then is fed into the generator to actually produce the resulting TS code. This way both subsystems can be developed and tested independently. This resembles very much the ViewModel
approach from the MVC web frameworks.
Design
Moto: check JSON data from APIs 100% type safe and at blazing speed.
Non-goals:
- Cover non-serializable types: at this stage of TS adoption most of the codebases that care about type safety have developed a safe "bubble" that only lets in checked values. This mainly means that the values get into the "bubble" throught a call to
JSON.parse()
that produces plain old data objects, this is where the 99% of type checking is required. - Cover a sophisticated schema verification protocol. While possible, the idea is to get an
unknown
type and turn it first into something type safe (safely assignable to a given type). The resulting value can get safely verified against a more sophisticated schema as a second step. Still, for simple checks the doors are open, but any non context free checks should be implemneted using a higher level schema verification generator that is not TypeScript specific. - Cover complex computed types or expensive JS values, except for generics (generics are neat and easy to cover in the current architecture).
Guiding principles:
- Type safety and correctness first.
- Performance second.
- The generated code should be readable and easy to modify by hand if needed.
- Common minifiers should be able to produce efficient and compact code
- KISS the generator to address the bugs and TypeScript updates quicker
- Use monomorphised functions to keep the JIT happy
Nice to haves:
- Languare server plugin / VS Code extension that "just" generates the predicate next to the type.
Tools used
- Foundational ts-ast-viewer.com
- Useful esbuild minifier