我在打字稿中使用通用JSON类型,建议从这里开始
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| {[key: string]: JSONValue}
我希望能够从与 JSON 匹配的接口类型转换为 JSON 类型。例如:
interface Foo {
name: 'FOO',
fooProp: string
}
interface Bar {
name: 'BAR',
barProp: number;
}
const genericCall = (data: {[key: string]: JSONValue}): Foo | Bar | null => {
if ('name' in data && data['name'] === 'FOO')
return data as Foo;
else if ('name' in data && data['name'] === 'BAR')
return data as Bar;
return null;
}
这目前失败了,因为 Typescript 看不到接口如何与 JSONValue 属于同一类型:
Conversion of type '{ [key: string]: JSONValue; }' to type 'Foo' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Property 'name' is missing in type '{ [key: string]: JSONValue; }' but required in type 'Foo'.
但从分析上讲,我们当然知道这是可以的,因为我们认识到在运行时类型Foo和Bar是JSON兼容的。我如何告诉打字稿这是一个好的演员表?
ETA:我可以按照错误消息先投射到未知,但我宁愿不这样做——如果 TS 真的理解其中的区别会更好,我想知道这是否可能。
这里的问题是编译器不使用检查if ('name' in data && data['name'] === 'FOO')
将data
类型从其原始{[key: string]: JSONValue}
类型缩小。 类型{[key: string]: JSONValue}
不是联合,目前in
运算符仅检查联合类型的窄值。 在microsoft/TypeScript#21732上有一个开放的功能请求来做这样的缩小,但现在它不是语言的一部分。
这意味着data
在检查后停留{[key: string]: JSONValue}
类型。然后,当您尝试通过data as Foo
断言data
属于Foo
类型时,编译器会警告您可能犯了一个错误,因为它看不到Foo
并且{[key: string]: JSONValue}
是足够相关的类型。
如果你确定你正在做的是一个很好的检查,你总是可以使用编译器建议和类型断言到与Foo
和{[key: string]: JSONValue}
相关的中间类型,例如unknown
:
return data as unknown as Foo; // okay
如果这与您有关,那么您可以编写自己的用户定义的类型保护函数,该函数执行您期望从if ('name' in data && data['name'] === 'FOO')
中缩小的范围。 本质上,如果该检查通过,那么我们知道data
属于类型{name: 'FOO'}
,其相关性足以Foo
类型断言。 下面是一个可能的类型保护函数:
function hasKeyVal<K extends PropertyKey, V extends string | number |
boolean | null | undefined | bigint>(
obj: any, k: K, v: V): obj is { [P in K]: V } {
return obj && obj[k] === v;
}
所以你写的不是if ('name' in data && data['name'] === 'FOO')
,而是if (hasKeyVal(data, 'name', 'FOO'))
. 返回类型obj is {[P in K]: V}
意味着如果函数返回true
,编译器应将obj
类型缩小到具有键类型为K
且值为V
类型的属性。 让我们测试一下:
const genericCall = (data: { [key: string]: JSONValue }): Foo | Bar | null => {
if (hasKeyVal(data, 'name', 'FOO'))
return data as Foo; // okay, data is now {name: 'FOO'} which is related to Foo
else if (hasKeyVal(data, 'name', 'BAR'))
return data as Bar; // okay, data is now {name: 'BAR'} which is related to Bar
return null;
}
现在它起作用了。hasKeyVal()
检查将data
缩小到具有正确类型的name
属性的内容,并且这足以Foo
或Bar
类型断言成功(类型断言仍然是必需的,因为如果Foo
具有其他属性,则类型{name: 'Foo'}
的值可能不是Foo
)。
操场链接到代码