我想说这看起来需要做很多工作,具体取决于您希望编译器能够为您做多少。我不确定是否存在用于 json 模式的现有 TS 类型集,它们足够丰富以表示从模式到输出类型的关系,因此您可能必须自己构建一些。以下是针对您的f1 和f2 示例专门定制的草图;其他用例可能需要对此处提供的代码进行一些修改/扩展,而且毫无疑问,在某些极端情况下事情不会按照您想要的方式进行。我将展示的代码的重点是展示一种通用方法,而不是针对任意 json 模式的完全成熟的解决方案。
这是Schema 的一种可能定义,对应于 json 模式对象的类型:
type Schema =
{ type: 'number' | 'integer' | 'string' } |
{
type: 'object',
required?: readonly PropertyKey[],
properties: { [k: string]: Schema }
};
一个Schema 有一个type 属性,属于string literal types 的某个联合,如果type 是object,那么它还有一个properties 属性,它是键到其他@ 的映射987654335@ 对象,它可能有一个 required 属性,它是一个键名数组。
可以使用conditional type 将Schema 转换为类型。有趣的部分是object 类型,它占据了下面代码的大部分复杂性:
type SchemaToType<S extends Schema> =
S extends { type: 'number' | 'integer' } ? number :
S extends { type: 'string' } ? string :
S extends { type: 'object', properties: infer O, required?: readonly (infer R)[] } ? (
RequiredKeys<
{ -readonly [K in keyof O]?: O[K] extends Schema ? SchemaToType<O[K]> : never },
R extends PropertyKey ? R : never
> & { [key: string]: any }) extends infer U ? { [P in keyof U]: U[P] } : never :
unknown;
type RequiredKeys<T, K extends PropertyKey> =
Required<Pick<T, Extract<keyof T, K>>> & Omit<T, K>
对于一个对象类型,SchemaToType 查找properties 和required 属性,并生成一个对象类型,其键来自properties,值递归地将SchemaToType 应用于其属性。这开始是完全可选的,但我们使用required 属性键并将所有可选对象转换为需要这些键的对象。那里使用了很多utility types:Pick、Omit、Extract、Required 等。详细写出它的工作原理需要很长时间,但重点是您可以通过编程方式进行转换Schema 的子类型转换为类型。
现在我们给createF 输入以下内容:
declare function createF<S extends Schema>(s: S): () => SchemaToType<S>;
并对其进行测试....但首先,请注意编译器通常会将您的架构对象类型扩展得太多而无用。如果我这样写:
const tooWideSchema = {
type: 'object', required: ["a"], properties: { a: { type: 'number' } }
};
编译器会推断它是这种类型:
// const tooWideSchema: {
// type: string; required: string[]; properties: { a: { type: string; }; };
// }
糟糕,编译器忘记了我们关心的东西:我们需要"object" 和"a" 和"number",而不是string!因此,在接下来的内容中,我将使用const assertions 要求编译器保持传入模式对象的推断类型尽可能窄:
const narrowSchema = {
type: 'object', required: ["a"], properties: { a: { type: 'number' } }
} as const;
as const 有很大不同:
// const narrowSchema: {
// readonly type: "object";
// readonly required: readonly ["a"];
// readonly properties: {
// readonly a: {
// readonly type: "number";
// };
// };
//}
这种类型现在已经有足够的细节来进行我们的转换了......所以让我们测试一下:
const f1 = createF({
type: 'integer',
} as const);
const t1 = f1();
// const t1: number
const f2 = createF({
type: 'object',
required: ["a"],
properties: {
a: { type: 'number' },
b: { type: 'string' },
},
} as const);
const t2 = f2();
/* const t2: {
[x: string]: any;
a: number;
b?: string | undefined;
} */
t1的类型推断为number,t2的类型推断为{[x: string]: any; a: number' b?: string | undefined }。这些基本上与您的 T1 和 T2 类型相同......耶!
这样就完成了演示。正如我上面所说,要小心其他用例和边缘情况。也许您会在这种方法上取得进展,或者最终您会发现为此使用类型系统过于脆弱和丑陋,而原始代码生成解决方案更适合您的需要。祝你好运!
Playground link to code