Original misterMicheels, licensed translation by New Frontend.
Why additional type checking?
TypeScript only performs static type checking at compile time! What you’re actually running is JavaScript compiled from TypeScript, and the resulting JavaScript knows nothing about types. Compile-time static type checking is useful inside the code base, but not for nonconforming input (for example, input received from an API).
Rigor of runtime checks
- You need to be at least as strict as compile-time checks, or you lose the assurance that compile-time checks provide.
- You can be stricter than compile-time checks if necessary, for example, age needs to be greater than or equal to 0.
Runtime type checking policy
Custom code check manually
- flexible
- It can be boring and error prone
- Easy to disconnect from actual code
Check manually using checkbox
For example, using joi:
import Joi from "@hapi/joi"
const schema = Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
age: Joi.number().integer().min(0).required()
});
Copy the code
- flexible
- Easy to write
- Easy to disconnect from actual code
Manually create a JSON Schema
Such as:
{
"$schema": "http://json-schema.org/draft-07/schema#"."required": [
"firstName"."lastName"."age"]."properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"type": "integer"."minimum": 0}}}Copy the code
- Using standard formats, there are a number of libraries available for validation.
- JSON is easy to store and reuse.
- It can be tedious, and writing JSON schemas by hand can be tedious.
- You need to ensure that the Schema and code are updated synchronously.
Automatically create JSON Schema
- Generate JSON schemas based on TypeScript code
- The typescript-json-schema tool, for example, can do this (both as a command-line tool and through code).
- You need to ensure that the Schema and code are updated synchronously.
- Generated based on JSON input sample
- Type information already defined in TypeScript code is not used.
- If the sample JSON input provided is inconsistent with the actual input, an error may occur.
- You still need to ensure that the Schema and code are updated synchronously.
translation
For example, ts-Runtime is used.
This approach translates code that is equivalent but has runtime type checking built in.
For example, the following code:
interface Person {
firstName: string;
lastName: string;
age: number;
}
const test: Person = {
firstName: "Foo",
lastName: "Bar",
age: 55
}
Copy the code
Would be translated as:
import t from "ts-runtime/lib";
const Person = t.type(
"Person",
t.object(
t.property("firstName", t.string()),
t.property("lastName", t.string()),
t.property("age", t.number())
)
);
const test = t.ref(Person).assert({
firstName: "Foo",
lastName: "Bar",
age: 55
});
Copy the code
The downside of this approach is that you have no control over where you do the runtime checking (we only do the runtime type checking at the boundary between input and output).
Incidentally, this is an experimental library and is not recommended for use in production environments.
Runtime types derive static types
For example, use the io-TS library.
In this way, we define runtime types, and TypeScript extrapolates static types from the runtime types we define.
Examples of runtime types:
import t from "io-ts";
const PersonType = t.type({
firstName: t.string,
lastName: t.string,
age: t.refinement(t.number, n => n >= 0, 'Positive')
})
Copy the code
Extract the corresponding static type from it:
interface Person extends t.TypeOf<typeof PersonType> {}
Copy the code
The above types are equivalent to:
interface Person {
firstName: string;
lastName: string;
age: number;
}
Copy the code
- Types are always synchronized.
- Io-ts is powerful, such as support for recursive types.
- You need to define the type as an IO-TS runtime type, which is not applicable when defining a class:
- An alternative is to use IO-TS to define an interface and then have the class implement that interface. However, this means updating the IO-TS type every time you add attributes to a class.
- It is not easy to reuse interfaces (such as using the same interface between the front and back ends) because they are of type IO-TS rather than normal TypeScript type.
Decorator-based class validation
For example, use the class-Validator library.
- Decorators based on class attributes.
- This is similar to Java’s JSR-380 Bean Validation 2.0, which Hibernate Validator implements.
- Other such Java EE-style libraries include TypeOrM (ORM libraries that resemble Java’s JPA) and routing-Controllers (apis that define Java’s JAX-RS).
Code examples:
import { plainToClass } from "class-transformer"; import { validate, IsString, IsInt, Min } from "class-validator"; class Person { @IsString() firstName: string; @IsString() lastName: string; @IsInt() @Min(0) age: number; } const input: any = { firstName: "Foo", age: -1 }; const inputAsClassInstance = plainToClass( Person, input as Person ); Validate (inputAsClassInstance). Then (errors => {// error handler});Copy the code
- Types are always synchronized.
- Useful when classes need to be checked.
- Can be used to examine the interface (define a class that implements the interface).
Note: Class-Validators are used for concrete class instances. In the code above, we use its sister library, class-Transformer, to convert ordinary input into a Person instance. The conversion itself does not do any type checking.