函数decode_json()
采用JSON并解码找到的任何对象中的所有键。
示例:JSON 中的a
键是latitude
(已解码)。为什么? a
在 alphabet
变量中的索引为 0,latitude
在 keys
变量中的索引也为 0。
问题
第 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;
}
最佳答案
注意:我只会研究最小的例子;如果需要,它可以扩展到您的原始版本,但我将把它留给您。
这里的大问题是你的函数的输出与其输入的类型肯定不同。如果您调用addBlabla(json)
哪里json
类型为{a: number}
,返回类型将为 {ablabla: number}
。这些不一样,所以如果 json
函数参数的类型为T
,您不能声称返回类型也是 T
.
如果输入类型为 T
,返回类型是什么? ,我们称之为Blabla<T>
?好吧,我们可以使用key remapping和 template 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
}
所以要小心!
关于typescript - 与输入和输出相同的泛型类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69771268/