You can define the following types. Don't use T extends Schema
in type SchemaToRecord<T>
!
If you use T extends Schema
, you will hide the actual value of each Field
. TypeScript will infer the types using the type Field
.
Not using T extends Schema
, means that you are delaying the inference the type of a concrete Field
value is provided for T
.
type Field = {
kind: 'string' | 'integer' | 'float' | 'bool';
required?: boolean;
};
type KindToType<T> = T extends 'string' ? string:
T extends 'integer' ? number :
T extends 'float' ? number :
T extends 'bool' ? boolean : never;
type GetMandatoryKeys<T> = {
[P in keyof T]: T[P] extends Exclude<T[P], undefined> ? P : never
}[keyof T]
type UndefinedToOptional<T> = Partial<T> & Pick<T, GetMandatoryKeys<T>>
type SchemaToRecord<T> = UndefinedToOptional<{
[K in keyof T]: 'required' extends keyof T[K] ?
('kind' extends keyof T[K] ? KindToType<T[K]['kind']> : never) :
('kind' extends keyof T[K] ? KindToType<T[K]['kind']> | undefined : never)
}>;
You can use the above types as follows:
const customerSchema = {
id: { kind: 'integer', required: true } as const,
name: { kind: 'string', required: true } as const,
debits: { kind: 'float' } as const,
}
type CustomerSchema = typeof customerSchema;
type CustomerRecord = SchemaToRecord<CustomerSchema>;
// OK
const customer: CustomerRecord = {
id: 1,
name: 'John Doe',
debits: 50
}
// OK
const customer: CustomerRecord = { id: 1, name: 'John Doe', debits: 50 };
// OK
const customer: CustomerRecord = { id: 1, name: 'John Doe' };
// ERROR Type 'string' is not assignable to type 'number'
const customer2: CustomerRecord = { id: 1, name: 'John Doe', debits: '50' };
// ERROR Type 'number' is not assignable to type 'string'
const customer3: CustomerRecord = { id: 1, name: 50 };
Don't use const customerSchema: Schema = {...
in const customerSchema = {
! use as const
(in each field) instead.
If you use : Schema
, TypeScript will infer the types using the Schema
(type), not the customerSchema
value. This will lead to less accurate type inference.