Reputation: 15
For my own learning I'm trying to create something like an ODM, but I'm having a lot of trouble figuring out how to create type definitions from a schema describing a type of document. For some reason type checking for instance
isn't working and suggestions don't work (instance
seems to act as if it's any
so the editor doesn't know what properties it has). Any help would be greatly appreciated, thank you.
type PropertyType = 'string' | 'number';
interface RequiredProperty<T> {
default: T;
optional?: false;
}
interface OptionalProperty<T> {
default?: T;
optional: true;
}
type BaseProperty<TKind extends PropertyType, TType> = (
| RequiredProperty<TType>
| OptionalProperty<TType>
) & {
kind: TKind;
};
type StringType = BaseProperty<'string', string>;
type NumberType = BaseProperty<'number', number>;
type ModelProperty = StringType | NumberType;
interface Model {
[x: string]: ModelProperty;
}
type ModelInstance<T extends Model> = {
[K in keyof T]: T[K]['kind'] extends 'string' ? string : number;
};
const model: Model = {
str: {
kind: 'string',
default: 'abc'
},
num: {
kind: 'number',
optional: true
}
};
const instance: ModelInstance<typeof model> = {
// Type 'string' is not assignable to type 'number'.
str: 'test',
num: 123
}
I'd want ModelInstance<typeof model>
to be something like:
interface Expected {
str: string;
num?: number;
}
Upvotes: 1
Views: 176
Reputation: 33051
You have two options to handle it.
First one
/**
* Please refer to this link for explanation
* https://stackoverflow.com/a/50375286
*/
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type PropertyType = 'string' | 'number';
interface RequiredProperty<T> {
default: T;
optional?: false;
}
interface OptionalProperty<T> {
default?: T;
optional: true;
}
type BaseProperty<Kind extends PropertyType, Type> = (
| RequiredProperty<Type>
| OptionalProperty<Type>
) & {
kind: Kind;
};
type StringType = BaseProperty<'string', string>;
type NumberType = BaseProperty<'number', number>;
type ModelProperty = StringType | NumberType;
interface Model {
[x: string]: ModelProperty;
}
type Values<T> = T[keyof T]
/**
* Translates string type name to actual type
* Logic is pretty straitforward
* - if ['kind'] is 'string' -> string
* - if ['kind'] is 'number' -> number
*/
type TranslateType<T extends { kind: PropertyType }> =
T['kind'] extends 'string'
? string
: T['kind'] extends 'number'
? number
: never;
type GenerateData<T extends Model> =
/**
* Iterate throus model data structure
*/
{
/**
* If ['optional'] exists and it is true
* Clone same data structure {kind:string, default:string}
* into nested property, make it partial and translate 'string' to string
*/
[K in keyof T]: T[K] extends { optional: true } ? {
-readonly [P in K]?: TranslateType<T[K]>
} : {
/**
* Do same as above but without making data optional
*/
-readonly [P in K]: TranslateType<T[K]>
}
};
/**
* UnionToIntersection -> converts union to UnionToIntersection
* Values -> obtain all nested properties as a union
*/
type ModelInstance<T extends Model> =
UnionToIntersection<Values<GenerateData<T>>>
const model = {
str: {
kind: 'string',
default: 'abc'
},
num: {
kind: 'number',
optional: true
}
} as const;
type Result = ModelInstance<typeof model>
Second
interface OptionRequired {
type: 'string' | 'number'
optional: boolean
}
interface OptionPartial {
type: 'string' | 'number'
}
type Option = OptionPartial | OptionRequired
/**
* Translates string type name to actual type
* Logic is pretty straitforward
*/
type TranslateType<T extends Option> =
T['type'] extends 'string'
? string
: T['type'] extends 'number'
? number
: never;
/**
* Check if optional exists
* if false - apply never, because union of T|never produces t
* if true - apply undefined
*/
type ModifierType<T extends Option> =
T extends { optional: true }
? undefined
: never
/**
* Apply TranslateType 'string' -> string
* Apply ModifierType {optional:true} -> undefined
*/
type TypeMapping<T extends Option> = TranslateType<T> | ModifierType<T>
/**
* Apply all conditions to each option
*/
type Mapping<T> = T extends Record<string, Option> ? {
-readonly [Prop in keyof T]: TypeMapping<T[Prop]>
} : never
type Data<Options> = Mapping<Options>
const model = {
a: {
type: 'string',
optional: true,
},
b: {
type: 'number',
optional: false
},
c: {
type: 'string',
},
} as const
type Result = Data<typeof model>
declare var x:Result
type a = typeof x.a // string | undefined
type b = typeof x.b // number
type c = typeof x.c // string
Personaly, I think that it is better to use required
property instead of optional
and accordingly reverse the boolean flags.
I mean to use {required:true}
instead of {optional: false}
.
It is more readable, but again, it is only my opinion.
Please take a look at this question. This case is VERY similar to yours
Upvotes: 2