如何从描述它的模式创建类型? [英] How can I create a type from a schema that describes it?
问题描述
为了我自己的学习,我正在尝试创建类似 ODM 的东西,但是我在弄清楚如何从描述文档类型的模式创建类型定义时遇到了很多麻烦.出于某种原因,instance
的类型检查不起作用并且建议不起作用(instance
似乎表现得好像它是 any
所以编辑器不知道它有什么属性).任何帮助将不胜感激,谢谢.
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
}
我希望 ModelInstance
类似于:
interface Expected {
str: string;
num?: number;
}
推荐答案
您有两个选择来处理它.
You have two options to handle it.
第一个
/**
* 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>
第二
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
就个人而言,我认为最好使用 required
属性而不是 optional
并相应地反转布尔标志.
Personaly, I think that it is better to use required
property instead of optional
and accordingly reverse the boolean flags.
我的意思是使用 {required:true}
而不是 {optional: false}
.
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
这篇关于如何从描述它的模式创建类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!