提问人:theahura 提问时间:12/6/2022 更新时间:12/7/2022 访问量:762
Typescript:扩展 JSON 类型以接受接口
Typescript: widening a JSON type to accept interfaces
问:
这是 Typescript 的扩展:将接口作为需要 JSON 类型的函数的参数传递(询问将接口传递给 JSON 类型化函数),而 Typescript 又是 Typescript 的扩展:扩展 JSON 类型的接口(询问是否向 JSON 类型/从 JSON 类型进行转换)
这些问题与 JSON Typescript 类型相关:
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| {[key: string]: JSONValue}
在 Typescript: passing interface as parameter for a function that need a JSON type 中,最终答案表明无法将接口传递给需要 JSON 值的函数。具体而言,以下代码:
interface Foo {
name: 'FOO',
fooProp: string
}
const bar = (foo: Foo) => { return foo }
const wrap = <T extends JSONValue[]>(
fn: (...args: T) => JSONValue,
...args: T
) => {
return fn(...args);
}
wrap(bar, { name: 'FOO', fooProp: 'hello'});
失败,因为无法将接口分配给该接口,即使从分析角度很容易识别强制转换应该没问题。Foo
JSONValue
查看游乐场和 https://github.com/microsoft/TypeScript/issues/15300
前面的回答是:
在不扩大 JSONValue 类型的情况下,我们唯一的解决方法是将 [interface] Foo 转换为类型。
就我而言,我可以修改 JSONValue 类型,但不能轻松修改所有相关接口。扩展 JSONValue 类型需要什么?
答:
如果对返回类型使用单独的泛型(这也扩展了),则不会遇到类型兼容性问题,并且还可以推断出更窄的返回类型:JsonValue
function wrap <A extends JsonValue[], T extends JsonValue>(
fn: (...args: A) => T,
...args: A
): T {
return fn(...args);
}
const result = wrap(bar, { name: 'FOO', fooProp: 'hello'}); // ok 👍
//^? const result: Foo
在您显示的类型上:对于具有键和值的联合成员(对象类型):更正确的做法是包含在 的并集中,使键有效地可选......因为对象中的每个键上永远不会有值,并且访问对象上不存在的键所产生的值是在运行时:JsonValue
string
JsonValue
undefined
JsonValue
undefined
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue | undefined };
// ^^^^^^^^^^^
这与 JavaScript 中 JSON 对象的序列化和反序列化算法兼容,因为它既不将属性序列化为值,也不支持 JSON
作为类型,因此永远不会存在于反序列化值中。undefined
undefined
undefined
此类型既方便(可以对任何属性名称使用点表示法属性访问),又是类型安全的(必须缩小每个值的范围才能安全地使用它)。JsonValue
更新以回应您的评论:
Playground 中的答案仍然取决于界面 Foo 是一种类型。但我需要它成为一个接口。这可能吗?
只有当接口扩展(受制于)类对象联合成员时,才有可能。JsonValue
TS 手册部分 Differences Between Type Aliases and Interfaces 以以下信息开头(重点是我的):
类型别名和接口非常相似,在许多情况下,您可以在它们之间自由选择。几乎所有的功能都可以在 中使用,关键的区别在于,不能重新打开类型来添加新属性,而接口始终是可扩展的。
interface
type
这意味着每个类型别名都是在定义它的站点上完成的......但是接口总是可以扩展(突变)的,所以在类型检查发生之前,确切的形状实际上不会最终确定,因为其他代码(例如,使用你的代码的其他代码)可以改变它。
防止这种情况的唯一方法是限制相关接口允许的扩展:
使用原始的、不受约束的接口的示例显示了编译器错误:
interface Foo {
name: 'FOO';
fooProp: string;
}
const bar = (foo: Foo) => foo;
const result = wrap(bar, { name: 'FOO', fooProp: 'hello'}); /*
~~~
Argument of type '(foo: Foo) => Foo' is not assignable to parameter of type '(...args: JsonValue[]) => JsonValue'.
Types of parameters 'foo' and 'args' are incompatible.
Type 'JsonValue' is not assignable to type 'Foo'.
Type 'null' is not assignable to type 'Foo1'.(2345) */
但是,如果接口被限制为仅允许与 的类对象联合成员兼容,则不存在潜在的类型兼容性问题:JsonValue
type JsonObject = { [key: string]: JsonValue | undefined };
//^ This type could be written with built-in type utilities a number of ways.
// I like this equivalent syntax:
// type JsonObject = Partial<Record<string, JsonValue>>;
interface Foo extends JsonObject {
name: 'FOO';
fooProp: string;
}
const bar = (foo: Foo) => foo;
const result = wrap(bar, { name: 'FOO', fooProp: 'hello'}); // ok 👍
另请参阅:TS 手册中的实用程序类型
评论
JsonValue
类型相关的 PR 的搜索查询结果。惯用 TypeScript 使用 PascalCase 作为类型名称。
我最初在回答中的意思是放宽类型。你可以满足于这种类型。JSONValue
object
const wrap = <T extends object[]>(
fn: (...args: T) => object,
...args: T
) => {
return fn(...args);
}
但是你基本上失去了类型安全性,因为该函数现在接受应该无效的类型,比如
interface Foo {
name: 'FOO',
fooProp: string,
fn: () => void
}
它具有具有函数类型的属性。理想情况下,我们不允许将此类型传递给函数。fn
但并不是所有的希望都消失了。我们只剩下一个选项:将类型推断为泛型类型并递归验证它。
type ValidateJSON<T> = {
[K in keyof T]: T[K] extends JSONValue
? T[K]
: T[K] extends Function // we will blacklist the function type
? never
: T[K] extends object
? ValidateJSON<T[K]>
: never // everything that is not an object type or part of JSONValue will resolve to never
} extends infer U ? { [K in keyof U]: U[K] } : never
ValidateJSON
采用某种类型并遍历其类型。它检查类型的属性,并将其解析为类型是否无效。T
never
interface Foo {
name: 'FOO',
fooProp: string,
fn: () => void
}
type Validated = ValidateJSON<Foo>
// {
// name: 'FOO';
// fooProp: string;
// fn: never;
// }
我们可以使用此实用程序类型来验证 内部的参数类型和返回类型。fn
wrap
const wrap = <T extends any[], R extends ValidateJSON<R>>(
fn: (...args: T) => R,
...args: { [K in keyof T]: ValidateJSON<T[K]> }
) => {
return fn(...args as any);
}
这都会导致以下行为:
// ok
wrap(
(foo: Foo) => { return foo },
{ name: 'FOO', fooProp: 'hello' }
);
// not ok, foo has a parameter type which includes a function
wrap(
(foo: Foo & { fn: () => void }) => { return foo },
{ name: 'FOO', fooProp: 'hello', fn: () => {} }
);
// not ok, fn returns an object which includes a function
wrap(
(foo: Foo) => { return { ...foo, fn: () => {} } },
{ name: 'FOO', fooProp: 'hello' }
);
// not ok, foo has a parameter type which includes undefined
wrap(
(foo: Foo & { c: undefined }) => { return foo },
{ name: 'FOO', fooProp: 'hello', c: undefined }
);
评论