【问题标题】:Types for overloaded recursive function重载递归函数的类型
【发布时间】:2020-07-20 09:49:11
【问题描述】:

我正在研究一个有 1 个参数的函数。参数要么是一个对象(可以嵌套),要么是一个对象数组。输入可能类似于

{
  key1: {
    key2: [{  
      key3: 1, 
      key4: { 
        key5: [...]
      }
    }]
  }
}

[
  { key1: 1, key2: 2, key3: { key4: 'val' }},
  { key5: { key6: { key7: 'val' }}}
]

输入的任何部分都可以是对象或数组。

为简单起见,此函数获取对象的所有键,并将New 附加到它们。

例如,带输入

{
  name: 'Name',
  email: 'name@example.com'
}

输出将是

{
  nameNew: 'Name',
  emailNew: 'name@example.com'
}

如果参数是一个数组,函数会映射数组并做同样的事情。

我遇到的问题是因为我不知道对象的嵌套级别,所以使用了递归函数。这是我到目前为止所拥有的。链接到playground

interface Object {
  [key: string]: Object | Object[];
}

function mapper(arg: Object[]): Object[];
function mapper(arg: Object): Object {
  if (Array.isArray(arg)) {
    return arg.map(mapper);
  }

  let result: Object = {};
  Object.keys(arg).forEach((key) => {
    let newKey: string = key + "New";
    let value = arg[key];

    if (value instanceof Array) {
      result[newKey] = value.map(mapper);
    } else if (Object.prototype.toString.call(arg) === "[object Object]") {
      result[newKey] = mapper(value);
    } else {
      result[newKey] = value;
    }
  });

  return result;
}

问题在于这段代码

  if (Array.isArray(arg)) {
    return arg.map(mapper);
  }

我对数组进行类型检查,所以如果输入是Object[],我只会进入块。当我映射输入并在每个元素上调用 mapper 时,Typescript 正在使用签名

function mapper(arg: Object[]): Object[]

当每个元素都是Object 时。我得到这个错误

Type 'Object[][]' is not assignable to type 'Object'.
  Index signature is missing in type 'Object[][]'.

有几个打字问题,但这是我无法解决的主要问题。

我认为 Typescript 可以正确推断打字,因为在 map 函数中使用 Object 调用 mapper。

【问题讨论】:

    标签: typescript recursion typescript-typings


    【解决方案1】:

    您的问题可能只是您没有正确使用overloads


    重载函数由函数的调用者看到的一组调用签名表示。调用签名没有实现(通常后跟分号;)。

    如果该函数有一个实现(而不仅仅是declared),那么这些调用签名之后应该跟一个实现签名实现功能。实现签名有一个实现,因此后跟一个大括号{

    请注意,实现签名不是调用签名之一。调用和实现签名之间的唯一关系是实现必须能够接受来自每个调用签名的参数,并且调用签名必须返回实现签名的返回类型的某个子类型。这种关系不是完全类型安全的,因此您需要小心返回类型。但需要注意的是,调用签名与实现签名是分开且不同的。


    无论如何,在您的原始代码中,您有一个(arg: Object[])=>Object[] 类型的调用签名和一个(arg: Object)=>Object 类型的实现签名。这些不兼容...并且大概您希望这两个都是调用签名并且不记得提供单独的实现签名。

    我建议您将两个签名都更改为调用签名,然后给它一个实现,该实现接受调用签名中 arg 类型的联合作为 arg,并返回来自调用签名的返回类型的联合调用签名:

    function mapper(arg: Object[]): Object[];
    function mapper(arg: Object): Object;
    function mapper(arg: Object | Object[]): Object | Object[] {
      // impl goes here
    }
    

    这应该可以清除错误。请注意,关于小心返回类型的警告是成立的。编译器只确保您返回Object | Object[]。如果您在传入Object 时返回Object[],它将不会注意到,反之亦然。所以小心点。

    好的,希望对您有所帮助;祝你好运!

    Playground link to code

    【讨论】:

      【解决方案2】:

      这是一个有趣的问题。这是我想出的:

      interface Object {
        [key: string]: string | Object | Object[];
      }
      
      function mapper(arg: Object): Object {
        
        let result: Object = {};
        Object.keys(arg).forEach((key) => {
          let newKey: string = key + "New";
          let value = arg[key];
      
          if (Array.isArray(value)) {
            result[newKey] = value.map(mapper);
          } else if (typeof value === 'object') {
            result[newKey] = mapper(value);
          } else {
            result[newKey] = value;
          }
        });
      
        return result;
      }
      
      
      const test: Object = {
        one: "hello",
        three: [{four: "hello"}, {five: "five"}, {six: {seven: "seven"}}]
      }
      
      const result = mapper(test);
      
      console.log(result);
      
      
      1. 更新了类型以包含像字符串这样的原语。在您的示例中,其中一些值只是原语,它们必须用于递归结束。您在代码中也有这种情况,但它没有反映在类型中。

      2. 不需要将数组作为参数。该函数始终采用单个 Object 作为参数。所以我们不需要第一个 if 语句。这可能会令人困惑,因为其中一些值是对象数组。但是,当您在这些值上 .map 时,您一次调用单个值的映射器,该值始终是单个对象,而不是对象数组。

      3. 我更新了函数核心中的一些 if 条件。您也许可以让它与其他人一起使用,但认为这更干净一些。

      4. 我在 typescript 操场上运行了一个小测试用例,根据您的描述,它似乎给了我们正确的行为,但请确认。

      ******编辑********

      感谢您的澄清。因此,如果您需要该函数同时采用ObjectObject[]。该函数还需要返回ObjectObject[]。这部分可能会有点混乱。因此,您可以将数组逻辑添加回您的函数。比如:

      if (Array.isArray(arg) {
         return arg.map(obj => mapper(obj));
      }
      

      这将引发类型错误。由于 mapper 现在可以返回ObjectObject[],就打字稿而言,这可能是Object[][],这不是我们想要的类型。如果您像这样进行类型转换,您可以解决这个问题:

      if (Array.isArray(arg) {
         return arg.map(obj => mapper(obj)) as Object[];
      }
      

      我们断言这将始终是 Object[] 的类型转换应该是正确的,除非您想支持嵌套数组。

      这可行,但从设计的角度来看,也许可以编写一个新函数来预先进行类型检查。它可以更清洁。比如:

      const mapperMap = (args: Object | Object[]): Object | Object[] => {
          if (Array.isArray(args)) {
            return args.map(obj => mapper(obj));
          }
          return mapper(args);
      }
      

      如果您以这种方式设置它,您可以进一步扩展它以处理其他类型或边缘情况。核心mapper 函数可以保持纯粹,并且会做一件事并做好。接收Object 并返回该Object 的映射版本。所有其他案例都可以组织在mapperMap 或任何有意义的名称中,想不到 更好的名字。

      【讨论】:

      • 我稍微澄清了我的问题。输入也可以是一个数组。使用您的代码,我收到此错误Argument of type 'Object[]' is not assignable to parameter of type 'Object'.
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-06
      • 1970-01-01
      • 2019-12-07
      • 1970-01-01
      • 2021-07-19
      相关资源
      最近更新 更多