Unified Adapter for Runtime Type Inference Libraries
This project is a prototype design for a unified type adapter for TypeScript Runtime Type libraries. This project aims to define a common adapter interface that can be used by frameworks for type inference and validation without the framework having to take on explicit library dependencies. The adapter is written in support of type inference dependency injection.
This project provides reference implementations for arktype, effect, io-ts, json-schema-to-ts, scale-codec, superstruct, typebox, valibot, yrel, yup and zod as well as tRPC client and server declarations to demonstrate a possible integration path for established frameworks.
The project is provided as a complimentary resource for the standard-schema working group. It seeks to explore a low impact design that would enable libraries to be integrated into down level frameworks without each library having to adopt a common type level interface.
License MIT
- TypeAdapter and TRPC
- TypeAdapter Interfaces
- TypeAdapter for Zod
- TypeAdapter for TypeBox
- TypeAdapter Example
- Prior Art
TypeAdapters consist of the following types and interfaces.
// Used to specify library static inference
export interface TypeInference {
input: unknown
output: unknown
}
// Used to specify runtime validation + hold adapter inference
export abstract class TypeAdapter<Inference extends TypeInference = TypeInference> {
inference: Inference = {} as never
abstract validate(schema: unknown, value: unknown): boolean
}
// Used to infer types on the adapter
export type Static<A, Type> = (
A extends TypeAdapter<infer I extends TypeInference>
? (I & { input: Type })['output']
: unknown
)
The following implements a type adapter for Zod.
import { TypeAdapter, TypeInference } from '@type-adapters/adapter'
import * as z from 'zod'
export interface Inference extends TypeInference {
output: this['input'] extends z.ZodSchema ? z.infer<this['input']> : unknown
}
export class Adapter extends TypeAdapter<Inference> {
validate(schema: z.ZodSchema, value: unknown): boolean {
return schema.safeParse(value).success
}
}
The following implements a type adapter for TypeBox.
import { TypeAdapter, TypeInference } from '@type-adapters/adapter'
import { TSchema, StaticDecode } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
export interface Inference extends TypeInference {
output: this['input'] extends TSchema ? StaticDecode<this['input']> : unknown
}
export class Adapter extends TypeAdapter<Inference> {
validate(schema: TSchema, value: unknown): boolean {
return Value.Check(schema, value)
}
}
The following defines a MethodBuilder that creates runtime type safe methods. The MethodBuilder accepts an abstract TypeAdapter interface via constructor argument which is used to infer method parameter and return type types. The example also shows Zod and TypeBox usage with the MethodBuilder.
The following aims to highlight framework
and type library
decoupling where only abstract TypeAdapter interfaces are known either side.
import { Static, TypeAdapter } from '@type-adapters/adapter'
interface MethodOptions {
input: unknown,
output: unknown
}
export type Method<Adapter extends TypeAdapter, Options extends MethodOptions> =
(input: Static<Adapter, Options['input']>) => Static<Adapter, Options['output']>
// Creates runtime type safe methods
export class MethodBuilder<Adapter extends TypeAdapter> {
// Adapter dependency injected on constructor
constructor(private readonly adapter: Adapter) { }
// Method return type inferred via Adapter + Input + Output properties.
public method<Options extends MethodOptions>(options: Options, callback: Method<Adapter, Options>): Method<Adapter, Options> {
return (input: unknown) => {
if(!this.adapter.validate(options.input, input)) { throw Error('invalid input') }
const output = callback(input as never)
if(!this.adapter.validate(options.output, output)) { throw Error('invalid output') }
return output
}
}
}
// ------------------------------------------------------------------
// Zod Usage
// ------------------------------------------------------------------
import * as z from '@type-adapters/zod'
const add = new MethodBuilder(new z.Adapter()).method({
input: z.object({
a: z.number(),
b: z.number()
}),
output: z.number()
}, ({ a, b }) => a + b)
add({ a: 1, b: 2 }) // 3
// ------------------------------------------------------------------
// TypeBox Usage
// ------------------------------------------------------------------
import * as t from '@type-adapters/typebox'
const sub = new MethodBuilder(new t.Adapter()).method({
input: t.Object({
a: t.Number(),
b: t.Number()
}),
output: t.Number()
}, ({ a, b }) => a + b)
sub({ a: 1, b: 2 }) // -1
Fastify Type Providers
https://fastify.dev/docs/latest/Reference/Type-Providers/
TypeSchema