api-contract-check
Size (builder): 3.94 KBSize (checker): 0.8 KB
This library is included in @arfa-tools/generator and builds serializable validation schemas from TypeScript types. It supports TypeScript aliases for resolving imports, as well as in-memory files (not yet saved to disk) for long transformation chains.
The library is designed for JSON-compatible contracts. Unsupported types fall back to any, which accepts any value. These fallbacks can be logged.
For most scenarios, PluginValidators is sufficient: it already provides an efficient way to use the library. If you cannot use the generator (for example, if you have your own generation pipeline or need custom logic), you can use the API directly.
The generated validation schemas can be used to validate API requests and responses, produce documentation, generate types for other languages, convert to validation schemas for third-party libraries, and more.
Supported types
any,number,object,boolean,string,null, and string, numeric, and boolean literals;- type aliases, interfaces, inline objects, and required and optional properties;
- inheritance from base interfaces without generic arguments: properties are merged;
- arrays (
T[],Array<T>), fixed-length tuples, andRecord<string, T>; - substitution of generic type alias parameters, such as
type Box<T> = { value?: T }; - indexed access with a specific object key (
User['id']) or an array element (UserList[number]); - unions and intersections of objects with compatible overlapping properties;
- object self-references within arrays, including unions and tuples:
children: Array<Node | string>; - enums with literal initializers and automatic numeric values, and
Enum.Memberreferences; - named imports, re-exports,
export *, and path aliases fromtsconfig.json.
This is enough to describe almost anything that can come from an external API.
Example of supported types and the generated validation schema
enum TypeStatus { Draft = 1, Published, Archived = 7 }
enum TypeChannel { Ready = 'READY', Waiting = 'WAITING' }
enum TypeOwner { Owner = "O'Hara" }
type TypeId = number;
type TypePair = object & { left: string; right?: TypeId }
type TypeTuple = [string, TypeId];
type TypeScores = Record<string, TypeId>;
type TypeVariant = { kind: 'named'; title: string } | { kind: 'sized'; size: TypeId };;
type TypeTreeNode = { name: string; children: Array<TypeTreeNode> };
interface TypeBaseDetails { base: string }
interface TypeInheritedDetails extends TypeBaseDetails { own: TypeId }
type TypeDetails = {
raw: any;
tags: Array<string>;
pair: TypePair;
exact: 'leaf';
count: number;
maybe: number | null;
mixed: TypeVariant | boolean;
tuple: TypeTuple;
labels: Array<'x' | 'y'>;
choice: string | number;
record: TypeScores;
inline: { note: string; marker?: 'hot' };
active?: boolean;
enumState: TypeStatus;
inherited: TypeInheritedDetails;
recursive: TypeTreeNode;
metaObject: object;
enumMember: TypeChannel.Ready;
escapedEnum: TypeOwner.Owner;
escapedLiteral: "can't";
};
type TypeDetailsAlias = TypeDetails;
type TypeItem = {
id: TypeId;
mode: TypeChannel;
details: TypeDetailsAlias
};
type TypeCollection = {
title: string;
level4: TypeItem;
variants: Array<TypeVariant>;
groups: Array<{
name: string;
level4Items: Array<{ code: string; detail: { note: string } }>;
}>;
};
type TypeData = {
flag: boolean;
state: TypeStatus;
level3: TypeCollection;
nullable: null;
literalCombo: 'open' | 3;
};
type TypePayload = {
requestId: string;
objectHint: object;
context: {
channel: TypeChannel;
batch: Array<TypeId>;
level2: TypeData
};
};
type TypeRequest = TypePayload;
type TypeResponse = TypePayload;const positiveRequestSchema = {
requestId: 'string',
objectHint: 'object',
context: {
channel: ['union', ['literal', 'READY'], ['literal', 'WAITING']],
batch: ['array', 'number'],
level2: {
flag: 'boolean',
state: ['union', ['literal', 1], ['literal', 2], ['literal', 7]],
literalCombo: ['union', ['literal', 'open'], ['literal', 3]],
nullable: ['literal', null],
level3: {
title: 'string',
variants: [
'array',
['union', { kind: ['literal', 'named'], title: 'string' }, { kind: ['literal', 'sized'], size: 'number' }],
],
groups: [
'array',
{
name: 'string',
level4Items: ['array', { code: 'string', detail: { note: 'string' } }],
},
],
level4: {
id: 'number',
mode: ['union', ['literal', 'READY'], ['literal', 'WAITING']],
details: {
exact: ['literal', 'leaf'],
escapedLiteral: ['literal', "can't"],
count: 'number',
raw: 'any',
metaObject: 'object',
active: ['optional', 'boolean'],
enumState: ['union', ['literal', 1], ['literal', 2], ['literal', 7]],
enumMember: ['literal', 'READY'],
escapedEnum: ['literal', "O'Hara"],
maybe: ['union', 'number', ['literal', null]],
tags: ['array', 'string'],
labels: ['array', ['union', ['literal', 'x'], ['literal', 'y']]],
choice: ['union', 'string', 'number'],
mixed: [
'union',
{ kind: ['literal', 'named'], title: 'string' },
{ kind: ['literal', 'sized'], size: 'number' },
'boolean',
],
pair: { left: 'string', right: ['optional', 'number'] },
record: ['record', 'number'],
tuple: ['tuple', 'string', 'number'],
inherited: { base: 'string', own: 'number' },
inline: { note: 'string', marker: ['optional', ['literal', 'hot']] },
recursive: ['recursiveArray', { name: 'string', children: ['array', ['self']] }],
},
},
},
},
},
} satisfies TypeSchema;
const positiveSchema = {
request: positiveRequestSchema,
response: positiveRequestSchema, // same as request
} satisfies TypeRootSchema;Unsupported types (fall back to any)
Promise<T>,Map<K, V>, and other unknown generic containers, generic interfaces, andextends Base<T>;Record<K, V>where the key is not specified directly asstring;- indexed access with dynamic or missing keys, and indexed access on tuples;
- mapped, conditional, and function types, computed property names, and computed enum initializers;
- optional and rest tuple elements, and incompatible intersections;
- direct circular references outside arrays, alias cycles, and recursive roots that are not objects;
- references through default or namespace imports; these imports are tracked only as file dependencies.
You can track fallbacks through getDegraded. If a union contains an any branch, the entire union becomes any.
Example of unsupported types and the generated validation schema
type UnsupportedMapped = { [K in 'a' | 'b']: number };
type UnsupportedPromise = Promise<string>;
type UnsupportedFunction = (value: string) => number;
type UnsupportedGenericBox = Set<boolean>;
type UnsupportedConditional = string extends number ? 1 : 2;
type UnsupportedVariadicTuple = [string, ...number[]];
interface UnsupportedMethodObject {
run(input: string): number;
}
interface UnsupportedIndexObject {
[key: string]: number;
}
interface UnsupportedPayload {
fn: UnsupportedFunction;
mapped: UnsupportedMapped;
promise: UnsupportedPromise;
indexed: UnsupportedIndexObject;
voidValue: void;
genericBox: UnsupportedGenericBox;
neverValue: never;
conditional: UnsupportedConditional;
symbolValue: symbol;
unknownValue: unknown;
variadicTuple: UnsupportedVariadicTuple;
undefinedValue: undefined;
methods: UnsupportedMethodObject;
}
type TypeRequest = UnsupportedPayload;
type TypeResponse = UnsupportedPromise;const fallbackSchema = {
request: {
fn: 'any',
mapped: 'any',
promise: 'any',
indexed: 'any',
voidValue: 'any',
genericBox: 'any',
neverValue: 'any',
conditional: 'any',
symbolValue: 'any',
unknownValue: 'any',
variadicTuple: 'any',
undefinedValue: 'any',
methods: { run: 'any' },
},
response: 'any',
} satisfies TypeRootSchema;