const optional = Symbol(); const GlobalObject = Object; const GlobalNumber = Number; const GlobalArray = Array; export namespace Type { export function String(args: { description?: string, enum?: S[] } = {}) { const result: TString = { type: 'string', }; if (args.enum) { result.enum = args.enum; } if (args.description) { result.description = args.description; } return result; } export function Number(args: { description?: string, enum?: N[] } = {}) { const result: TNumber = { type: 'number', }; if (args.enum) { result.enum = args.enum; } if (args.description) { result.description = args.description; } return result; } export function Integer(args: { description?: string, enum?: N[] } = {}) { const result: TNumber = { type: 'integer', }; if (args.enum) { result.enum = args.enum; } if (args.description) { result.description = args.description; } return result; } export function Boolean(args: { description?: string } = {}) { const result: TBoolean = { type: 'boolean', }; if (args.description) { result.description = args.description; } return result; } export function Array(items: T, args: { description?: string } = {}) { const result: TArray = { type: 'array', items, } if (args.description) { result.description = args.description; } return result; } export function Optional(scheme: T): TOptional { const result = { ...scheme }; GlobalObject.defineProperty(result, optional, { value: true, enumerable: false, writable: false, configurable: false }); return result as unknown as TOptional; } export function Object(properties: T, args: { description?: string } = {}) { const result: TObject = { type: 'object', properties, required: GlobalObject.keys(properties) .filter(key => !(properties[key] as any)[optional]), } if (args.description) { result.description = args.description; } return result; } export function Enum>(items: S[], args?: { description?: string }): T; export function Enum>(items: N[], args?: { description?: string }): T; export function Enum(items: (number | string)[], args: { description?: string } = {}) { if (typeof items?.[0] === 'number') { return Number({ enum: items.map(item => +item!), ...args }); } return String({ enum: items.map(item => item!.toString()), ...args }); } export interface CheckError { path: string; message: string; } function check(scheme: TScheme, value: unknown, path: string): CheckError[] { if (value == null) { if ((scheme as any)[optional]) return []; return [{ path, message: `Expected ${scheme.type} at ${path}, got ${value}` }]; } switch (scheme.type) { case 'string': { if (typeof value !== 'string') { return [{ path, message: `Expected string at ${path}, got ${typeof value}` }]; } if (scheme.enum && !scheme.enum.includes(value)) { return [{ path, message: `Expected one of [${scheme.enum.join(', ')}] at ${path}, got "${value}"` }]; } return []; } case 'number': case 'integer': { if (typeof value !== 'number') { return [{ path, message: `Expected number at ${path}, got ${typeof value}` }]; } if (scheme.type === 'integer' && !GlobalNumber.isInteger(value)) { return [{ path, message: `Expected integer at ${path}, got ${value}` }]; } const s = scheme as TNumber; if (s.enum && !s.enum.includes(value)) { return [{ path, message: `Expected one of [${s.enum.join(', ')}] at ${path}, got ${value}` }]; } return []; } case 'boolean': { if (typeof value !== 'boolean') { return [{ path, message: `Expected boolean at ${path}, got ${typeof value}` }]; } return []; } case 'array': { if (!GlobalArray.isArray(value)) { return [{ path, message: `Expected array at ${path}, got ${typeof value}` }]; } const s = scheme as TArray; return value.flatMap((item, i) => check(s.items, item, `${path}[${i}]`)); } case 'object': { if (typeof value !== 'object' || GlobalArray.isArray(value)) { return [{ path, message: `Expected object at ${path}, got ${typeof value}` }]; } const s = scheme as TObject; const errors: CheckError[] = []; for (const key of s.required || []) { if (!(key in (value as Record))) { errors.push({ path: `${path}.${key}`, message: `Missing required property ${path}.${key}` }); } } for (const [key, propScheme] of GlobalObject.entries(s.properties)) { const propValue = (value as Record)[key]; if (propValue === undefined) continue; errors.push(...check(propScheme, propValue, `${path}.${key}`)); } return errors; } default: return []; } } export function Check(scheme: TScheme, value: unknown): CheckError[] { return check(scheme, value, '$'); } export function Is(scheme: T, value: unknown): value is Static { return Check(scheme, value).length === 0; } } export interface TString { type: 'string'; enum?: T[]; description?: string; } export interface TNumber { type: 'number' | 'integer'; enum?: T[]; description?: string; } export interface TBoolean { type: 'boolean'; description?: string; } export interface TArray { type: 'array'; description?: string; items: T; } export type TProperties = Record; export interface TObject { type: 'object'; description?: string; properties: T; required?: string[]; } export interface TOptionalString extends TString { [optional]: true; } export interface TOptionalNumber extends TNumber { [optional]: true; } export interface TOptionalBoolean extends TBoolean { [optional]: true; } export interface TOptionalArray extends TArray { [optional]: true; } export interface TOptionalObject extends TObject { [optional]: true; } export type TOptional = T extends TString ? TOptionalString : T extends TNumber ? TOptionalNumber : T extends TBoolean ? TOptionalBoolean : T extends TArray ? TOptionalArray : T extends TObject ? TOptionalObject

: never; export type IsOptional = T extends { [optional]: true } ? true : false; export type TScheme = TString | TNumber | TBoolean | TArray | TObject | TOptionalString | TOptionalNumber | TOptionalBoolean | TOptionalArray | TOptionalObject; type Prettify = { [K in keyof T]: T[K] } & {}; type RequiredKeys = { [K in keyof T]: IsOptional extends true ? never : K }[keyof T]; type OptionalKeys = { [K in keyof T]: IsOptional extends true ? K : never }[keyof T]; type StaticObject = Prettify< { [K in RequiredKeys]: Static } & { [K in OptionalKeys]?: Static } >; export type Static = T extends TString ? S : T extends TNumber ? N : T extends TBoolean ? boolean : T extends TArray ? Static[] : T extends TObject ? StaticObject

: never;