代码之家  ›  专栏  ›  技术社区  ›  Nino Filiu

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

  •  1
  • Nino Filiu  · 技术社区  · 6 年前

    我有工厂功能 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 分别,但未键入: 创建F 不是为了TS推断正确的类型而编写的 f1级 和 f2层 . 可以重写一下吗 创建F 所以它是这样的?如果是,如何进行?

    我知道有可能 have a return type that depends on parameters 通过使用函数重载,但在我的例子中,所有可能的输入都是JSON模式,我不知道如何将函数重载解决方案扩展到这种情况。

    目前,我使用 json-schema-to-typescript 在编译时围绕由创建的函数生成类型 创建F ,但这并不理想。


    一点上下文,以避免 XY problem :我实际上正在建造 oa-client ,一个基于OpenAPI规范创建助手的库,该规范包含模式。在运行时,创建的助手只接受并返回模式中定义的对象;但是在TS层上没有类型-我必须使用模式来使用节点脚本编写TS,这并不理想,尤其是因为 oa客户端 就是不生成代码。

    0 回复  |  直到 5 年前
        1
  •  4
  •   jcalz    6 年前

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


    这里有一个可能的定义 Schema ,对应于json架构对象的类型:

    type Schema =
      { type: 'number' | 'integer' | 'string' } |
      { 
         type: 'object', 
         required?: readonly PropertyKey[], 
         properties: { [k: string]: Schema } 
      };
    

    A. 架构 有一个 type 的某个并集的性质 string literal types ,如果 类型 是 object ,则它还具有 properties 属性,该属性是键到其他 架构 对象,它可能具有 required 属性,它是键名称的数组。

    a的翻译 架构 可以使用 conditional type . 有趣的是 对象 类型,它占用了下面代码的大部分复杂性:

    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 查找 属性 和 必修的 属性,并生成具有键的对象类型 属性 和递归应用的值 模式类型 其属性。这一开始是完全可选的,但我们使用 必修的 属性键,并将所有可选对象转换为需要这些键的对象。有很多 utility types 在那里使用: Pick , Omit , Extract , Required ,等等。详细写出它的工作原理需要很长时间,但关键是您可以通过编程方式转换 架构 到类型。


    现在我们给 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

        2
  •  1
  •   jered    6 年前

    这似乎是一个微不足道的问题 generics . 我不太确定你的 createF 函数体看起来像,但您可以使用泛型 <T> 该类型的 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;
    };
    

    TypeScript现在将根据传递给的参数推断返回函数的类型 createF() :

    enter image description here

    泛型与声明类型具有参数类似,类似于传统函数。与函数一样,“参数”(或“泛型”)没有值(或类型),除非您声明该类型的值。