Typescript:扩展 JSON 类型的接口



我在打字稿中使用通用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和BarJSON兼容的。我如何告诉打字稿这是一个好的演员表?

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属性的内容,并且这足以FooBar类型断言成功(类型断言仍然是必需的,因为如果Foo具有其他属性,则类型{name: 'Foo'}的值可能不是Foo)。

操场链接到代码

最新更新