typescript - 与输入和输出相同的泛型类型

标签 typescript

函数decode_json()采用JSON并解码找到的任何对象中的所有键。

示例:JSON 中的a 键是latitude(已解码)。为什么? aalphabet 变量中的索引为 0,latitudekeys 变量中的索引也为 0。

TS playground link HERE

问题

第 4 行

TS2322: Type 'any[]' is not assignable to type 'T1'.
'T1' could be instantiated with an arbitrary type which could be unrelated to 'any[]'.

代码

const alphabet = 'abcdefghijklmnopqrstuvwxyz';

export default function decode_json<T1>(json: T1): T1 {
  if (Array.isArray(json)) return json.map(decode_json);

  if (typeof json === 'object')
    return Object.entries(json).reduce<{ [key: string]: T1 }>((l, r) => {
      if (Array.isArray(r[1])) {
        l[decodeKey(r[0])] = r[1].map(decode_json);
        return l;
      }

      if (typeof r[1] === 'object') {
        l[decodeKey(r[0])] = decode_json(r[1]);
        return l;
      }

      l[decodeKey(r[0])] = r[1];

      return l;
    }, {});

  return json;
}

function decodeKey(key: string): string {
  if (alphabet.indexOf(key) !== -1) return keys[alphabet.indexOf(key)];

  throw new Error(`The key "${key}" does not exist.`);
}

const keys: string[] = [
  'latitude',
  'longitude',
];

最小示例

函数会将“blabla”作为后缀添加到对象或对象数组中的每个键

function addBlabla<T1>(json: T1): T1 {
  if (Array.isArray(json)) return json.map(addBlabla);

  if (typeof json === "object")
    return Object.entries(json).reduce<{ [key: string]: T1 }>((l, r) => {
      l[r[0] + "blabla"] = r[1];

      return l;
    }, {});

  return json;
}

TS playground

最佳答案

注意:我只会研究最小的例子;如果需要,它可以扩展到您的原始版本,但我将把它留给您。


这里的大问题是你的函数的输出与其输入的类型肯定不同。如果您调用addBlabla(json)哪里json类型为{a: number} ,返回类型将为 {ablabla: number} 。这些不一样,所以如果 json函数参数的类型为T ,您不能声称返回类型也是 T .

如果输入类型为 T,返回类型是什么? ,我们称之为Blabla<T> ?好吧,我们可以使用key remappingtemplate literal types来表达所发生的事情。这是一种方法:

type Blabla<T> = T extends readonly any[] ? { [K in keyof T]: Blabla<T[K]> } :
  T extends object ? { [K in keyof T as `${Exclude<K, symbol>}blabla`]: T[K] } : T;

这是一个conditional type检查是否 T类似于数组( readonly any[]any[] 更宽松),如果是这样, maps it to a new array其中每个元素都有 Blabla<>应用于他们。

如果T是一个非类数组对象,我们重新映射 K 中的每个键至`${Exclude<K, symbol>}blabla` 。这使用模板文字类型来附加 "blabla"到键的末尾。请注意 keyof T can include symbol -typed keys但模板文字类型不能附加到符号,因此我们使用 the Exclude<T, U> utility type抑制任何 symbol - 值键。 (在您的实现中, Object.entries() 无论如何都会排除 symbol 键,所以这是正确的做法。)

最后,如果 T不是数组或对象,该类型的计算结果仅为 T .

所以类型为 addBlaBla看起来像:

function addBlabla<T>(json: T): Blabla<T> { /* impl */ }

此时,如果您按原样使用实现,您将开始遇到与以前相同的错误。在实现内部,输入 T是一个未指定 generic类型参数。并且编译器无法对此类类型进行非常复杂的分析。

当你写的时候

if (Array.isArray(json)) return json.map(addBlabla);

编译器可以使用control flow analysis缩小json来自类型T类似 T & any[] ,它可以让您调用 map() 。但它做的是缩小类型参数 T本身。所以它不知道是否 json.map(addBlabla)将导致Blabla<T> .

类型Blabla<T>对于 addBlabla() 的实现中的编译器来说本质​​上是不透明 。这是 Typescript 中当前或永久的设计限制。 GitHub 中关于此问题的规范问题可能是 microsoft/TypeScript#33912 ,但还有其他相关问题。目前,我们不能指望编译器能够确定任何特定值都可以分配给Blabla<T>。对于未指定的通用T .


这意味着我们需要解决这个问题。如果知道某个值的类型,但编译器不知道,您可以使用 type assertions告诉编译器它不知道什么:

function addBlabla<T>(json: T): Blabla<T> {
  if (Array.isArray(json)) return json.map(addBlabla) as any as Blabla<T>;

  if (typeof json === "object")
    return Object.entries(json).reduce<{ [key: string]: T }>((l, r) => {
      l[r[0] + "blabla"] = r[1];
      return l;
    }, {}) as Blabla<T>;

  return json as Blabla<T>;
}

现在编译没有错误。您需要小心,不要对编译器撒谎,因此您应该在这样做之前三次检查断言是否准确。例如,即使实现错误,以下代码编译也不会出现错误:

function addBlablaBad<T>(json: T): Blabla<T> {
  return json as Blabla<T>; // this is a lie, but no error
}

不得不使用as Blabla<T>有点烦人在代码中三次,所以我们可以做一些几乎等同于类型断言的事情:单调用签名 overloaded function :

function addBlabla<T>(json: T): Blabla<T>;
function addBlabla<T>(json: T) {
  if (Array.isArray(json)) return json.map(addBlabla);

  if (typeof json === "object")
    return Object.entries(json).reduce<{ [key: string]: T }>((l, r) => {
      l[r[0] + "blabla"] = r[1];
      return l;
    }, {});

  return json;
}

重载函数实现的检查比常规函数更宽松,因此编译器对上述内容感到满意。它与类型断言一样不安全,因此您应该像以前一样检查实现。例如,这里没有错误:

function addBlablaBad<T>(json: T): Blabla<T>;
function addBlablaBad<T>(json: T) {
  return json; // this is a lie, but no error
}

所以要小心!


Playground link to code

关于typescript - 与输入和输出相同的泛型类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69771268/

相关文章:

组件级别的 Angular 模拟服务

typescript - vscode 激活扩展失败 : Cannot find module with non-relative import

javascript - Vue + TypeScript - 用于放置自定义类型的推荐文件夹结构约定?

typescript - 如何使用表单数据发布Axios

reactjs - 没有 Prop 时传递给 super 什么?

typescript - Java相当于javascript map方法,有两个参数,第二个是索引

javascript - 如何修复 process.nextTick 不是 Calgolia place.js 的函数?

javascript - 无法在 Angular 4 中遍历 Map

angular - Angular `ngOnInit` 中的异步/等待

javascript - 浏览器使用错误路径加载 Typescript js.map 文件