Javascript 基于作为参数传递的JSON模式键入函数

Javascript 基于作为参数传递的JSON模式键入函数,javascript,json,typescript,jsonschema,Javascript,Json,Typescript,Jsonschema,我有一个工厂函数createF,它接受一个JSON模式作为输入,并输出一个函数f,该函数返回符合此模式的对象,如下所示: const createF = (schema) => { /* ... */ } type T1 = number; const f1: T1 = createF({ type: 'integer', }); type T2 = { a: number; b?: string; [key: string]: any; }; const f2: T2

我有一个工厂函数
createF
,它接受一个JSON模式作为输入,并输出一个函数
f
,该函数返回符合此模式的对象,如下所示:

const createF = (schema) => { /* ... */ }

type T1 = number;
const f1: T1 = createF({
  type: 'integer',
});

type T2 = {
  a: number;
  b?: string;
  [key: string]: any;
};
const f2: T2 = createF({
  type: 'object',
  required: ['a'],
  properties: {
    a: { type: 'number' },
    b: { type: 'string' },
  },
});
f1
f2
始终返回形状分别类似于
T1
T2
的对象,但它们未被键入:
createF
未被写入,因此TS推断出
f1
f2
的正确类型。有没有可能重写
createF
,这样就可以了?如果是,如何进行

我知道可以通过使用函数重载来实现,但在我的例子中,所有可能的输入都是JSON模式,我不知道如何将函数重载解决方案扩展到这种情况

目前,我使用在编译时围绕
createF
创建的函数生成类型,但这并不理想



有一点要避免的上下文:我实际上正在构建一个库,它基于包含模式的OpenAPI规范创建一个助手。在运行时,创建的帮助器只接受并返回模式中定义的对象;但是在TS层上没有类型-我必须使用模式来使用节点脚本编写TS,这并不理想,特别是因为的目标是不生成代码。

这对我来说似乎是一个微不足道的问题。我不确定您的
createF
函数体将是什么样子,但是您可以看到,通过使用泛型
,可以保留
schema
参数的类型,并用于确定返回函数的类型。您甚至不需要更改调用
createF()
的方式,只需更改函数声明本身,甚至不需要太多:

function createF<T> (schema: T) {
    return () => schema;
}

const f1 = createF({
  type: 'integer',
});
type T1 = number;

const f2 = createF({
  type: 'object',
  required: ['a'],
  properties: {
    a: { type: 'number' },
    b: { type: 'string' },
  },
});
type T2 = {
  a: number;
  b?: string;
  [key: string]: any;
};
函数createF(模式:T){
return()=>schema;
}
常数f1=createF({
键入:“整数”,
});
T1型=数量;
常数f2=createF({
类型:“对象”,
必需:['a'],
特性:{
a:{type:'number'},
b:{type:'string'},
},
});
T2型={
a:数字;
b:字符串;
[键:字符串]:任意;
};
TypeScript现在将根据传递给
createF()
的参数推断返回函数的类型:


泛型在某种程度上类似于将类型声明为具有参数,类似于传统函数。与函数一样,“参数”(或“泛型”)也没有值(或类型),除非您声明该类型的值。

我想说,这看起来需要做很多工作,这取决于您希望编译器能够为您做多少工作。我不确定是否存在一组足够丰富的json模式的TS类型来表示模式到输出类型之间的关系,因此您可能需要自己构建一些。以下是围绕
f1
f2
示例专门定制的草图;其他用例可能需要对此处提供的代码进行一些修改/扩展,毫无疑问,在一些边缘用例中,事情并没有按照您希望的方式进行。我将展示的代码的要点是展示一种通用方法,而不是针对任意json模式的完全烘焙的解决方案


下面是一个可能的
Schema
定义,对应于json模式对象的类型:

type Schema =
  { type: 'number' | 'integer' | 'string' } |
  { 
     type: 'object', 
     required?: readonly PropertyKey[], 
     properties: { [k: string]: Schema } 
  };
Schema
具有某种并集的
type
属性,如果该
type
object
,则它还具有
properties
属性,该属性是键到其他
Schema
对象的映射,并且它可能具有
required
属性,该属性是键名数组

可以使用。有趣的部分是
对象
类型,它占据了下面代码的大部分复杂性:

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>
然后测试一下。。。。但首先,请注意,编译器通常会将您的模式对象类型扩展得太多而没有用处。如果我这样写:

const tooWideSchema = { 
  type: 'object', required: ["a"], properties: { a: { type: 'number' } } 
};
编译器将推断它是以下类型:

// const tooWideSchema: { 
//   type: string; required: string[]; properties: { a: { type: string; }; }; 
// }
哎呀,编译器忘记了我们关心的东西:我们需要的是
“object”
“a”
“number”
,而不是
字符串
!因此,在下面的内容中,我将要求编译器将传入的模式对象的推断类型保持得尽可能窄:

const narrowSchema = { 
  type: 'object', required: ["a"], properties: { a: { type: 'number' } } 
} 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
类型相同。。。耶


演示到此结束。正如我上面所说的,小心其他用例和边缘用例。也许您会在这种方法上取得进展,或者最终您会发现使用类型系统来实现这一点过于脆弱和丑陋,而原始的代码生成解决方案更适合您的需要。祝你好运


我知道泛型;)感谢您的努力,但这并没有解决问题,您的解决方案将
f1
类型设置为
()=>{type:string}
,但问题精确到了预期的
()=>T1
类型,即
()=>number
,这就是整个问题所在-如何仅使用TS将JSON模式转换为类型?或者怎么做?你是对的,我没有正确回答你的问题,很抱歉。我能想到的最接近的事情是PropTypes如何拥有一个实用程序类型,从PropTypes定义推断TS类型。也许是类似的?令人惊叹的!巧妙地使用条件类型和常量断言(我都不知道)正是我所需要的
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;
} */