To start off, you'll need to define the types for your schema definitions. Here's a suggested structure:
interface SchemaOptional { optional?: boolean }
interface SchemaNumber extends SchemaOptional { type: 'uint8' | 'int8' | 'float' }
interface SchemaString extends SchemaOptional { type: 'string' }
interface SchemaObject extends SchemaOptional {
type: 'object',
fields: Record<string, SchemaEntry>
}
type SchemaEntry = SchemaObject | SchemaNumber | SchemaString
This set of interfaces assigns a specific type to each entry in the schema, with SchemaEntry
serving as a union of all possible types.
Note that the SchemaObject
includes fields
, which is a collection of keys and other schema entries, enabling recursive nesting.
Next, we'll create a type to convert each entry to its appropriate data type.
type SchemaToType<T extends SchemaEntry> =
T extends SchemaNumber ? number :
T extends SchemaString ? string :
T extends SchemaObject ? SchemaObjectToType<T> :
never
This conditional type evaluates if T
matches a certain entry type and returns the corresponding data type. For string
or number
entries, it directly provides those, while for object
entries, it invokes the SchemaObjectToType
type, as outlined below:
type SchemaObjectToType<T extends SchemaObject> = {
[K in keyof T['fields']]:
| SchemaToType<T['fields'][K]>
| SchemaEntryIsOptional<T['fields'][K]>
}
The above mapping assigns data types to all properties within the fields
object by utilizing the SchemaToType
function to recursively determine their actual types.
The final line ensures optional requirements are appropriately handled. Let's delve into this supporting type:
type SchemaEntryIsOptional<T extends SchemaEntry> =
T['optional'] extends true ? undefined : never
This type will return undefined
if the property is optional, or never
if it's mandatory. Combining with undefined
mimics an optional feature adequately.
Let's run some simple tests:
type TestA = SchemaToType<{ type: 'string' }>
// string
type TestB = SchemaToType<{ type: 'uint8' }>
// number
type TestC = SchemaToType<{ type: 'object', fields: { a: { type: 'string' }}}>
// { a: string }
type TestD = SchemaToType<{ type: 'object', fields: { a: { type: 'string', optional: true }}}>
// { a: string | undefined }
Everything seems to be functioning well. What about your schema1
object?
const schema1 = {
type: 'object',
fields: {
type: { type: 'uint8' },
title: { type: 'string', optional: true }
}
} as const;
type Schema1Type = SchemaToType<typeof schema1>
/*
type Schema1Type = {
readonly type: number;
readonly title: string | undefined;
}
*/
All appears to be working correctly!
Playground Link