Runtime type checking in TypeScript
An overview of ways to add runtime type checking to TypeScript applications
Why additional type checking?
TypeScript only performs static type checking at compile time! The generated JavaScript, which is what actually runs when you run your code, does not know anything about the types.
- Works fine for type checking within your codebase
- Doesn’t provide any kind of protection against malformed input (for example, when receiving input from API)
- Isn't designed to express typical input validation constraints (minimum array length, string matching a certain pattern) that are about more than simple type safety
- Several of the methods below provide an easy way to specify these kinds of constraints together with the actual TypeScript types
What about type guards?
Type guards are a way to provide information to the TypeScript compiler by having the code check values at runtime.
- Some degree of runtime type checking
- Often, type guards combine information available at runtime with information from type declarations specified in the code. The compiler will make incorrect assumptions if the actual input doesn't match those type declarations.
See also Type guards
Strictness of runtime checking
- Needs to be at least as strict as compile-time checking (otherwise, we lose the guarantees that the compile-time checking provides)
- Can be more strict if desired (require age to be >= 0, require string to match a certain pattern)
- Note that the TypeScript compiler will not be able to rely on such information
Runtime type checking strategies
Manual checks in custom code
- Flexible
- Can be tedious and error-prone
- Can easily get out of sync with actual code
Manual checks using a validation library
Example validation library: joi
- Flexible
- Easy to write
- Can easily get out of sync with actual code
Manually creating JSON Schemas
Example JSON Schema:
- Standard format, lots of libraries available for validation ...
- JSON: easy to store and share
- Can become very verbose and they can be tedious to generate by hand
- Need to make sure Schemas and code stay in sync!
Automatically generating JSON Schemas
Generating JSON Schemas from TypeScript code
Most robust library at the moment: ts-json-schema-generator (for some alternatives, see this discussion )
Example input, including specific constraints that are stricter than TS type checking:
Example output (with default options):
Some benefits/drawbacks:
- Single source of truth
- Need to make sure generated schemas and code stay in sync!
Transpilation
Example library: ts-runtime
- Processes code
- Transpiles code into equivalent code with built-in runtime type checking
Example code:
Example transpiled code
Problem: no control over where type checking happens (we only need runtime type checks at the boundaries!)
Note: Library is still in an experimental stage and not recommended for production use!
Deriving static types from runtime types
Example library: io-ts
- You define runtime types
- TypeScript infers the corresponding static types from these
Example runtime type:
Extracting the corresponding static type:
Equivalent to:
- No possibility for types to get out of sync
- io-ts is pretty powerful, supports recursive types etc.
- Requires you to define your types as io-ts runtime types, which does not work when you are defining classes
- One way to handle this could be to define an interface using io-ts and then make the class implement the interface. However, this means you need to make sure to update the io-ts type whenever you are adding properties to your class.
- Harder to share interfaces (e.g. between backend and frontend) because they are io-ts types rather than plain TypeScript interfaces
Decorator-based class validation
Example library: class-validator
- Uses decorators on class properties
- Very similar to Java’s JSR-380 Bean Validation 2.0 (implemented by, for example, Hibernate Validator)
- Part of a family of Java EE-like libraries that also includes typeorm (ORM, similar to Java’s JPA) and routing-controllers (similar to Java’s JAX-RS for defining APIs)
Example code:
- No possibility for types to get out of sync
- Good for checking classes
- Can be useful for checking interfaces by defining a class implementing the interface
Note: class-validator needs actual class instances to work on
- Here, we used its sister library class-transformer to transform our plain input into an actual
Person
instance.- The transformation in itself does not perform any kind of type checking!