Typescript:扩展 JSON 类型以接受接口

Typescript: widening a JSON type to accept interfaces

提问人:theahura 提问时间:12/6/2022 更新时间:12/7/2022 访问量:762

问:

这是 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'});

失败,因为无法将接口分配给该接口,即使从分析角度很容易识别强制转换应该没问题。FooJSONValue

查看游乐场https://github.com/microsoft/TypeScript/issues/15300

前面的回答是:

在不扩大 JSONValue 类型的情况下,我们唯一的解决方法是将 [interface] Foo 转换为类型。

就我而言,我可以修改 JSONValue 类型,但不能轻松修改所有相关接口。扩展 JSONValue 类型需要什么?

JSON TypeScript 类型 接口 转换

评论


答:

0赞 jsejcksn 12/6/2022 #1

如果对返回类型使用单独的泛型(这也扩展了),则不会遇到类型兼容性问题,并且还可以推断出更窄的返回类型: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

在您显示的类型上:对于具有键和值的联合成员(对象类型):更正确的做法是包含在 的并集中,使键有效地可选......因为对象中的每个键上永远不会有值,并且访问对象上不存在的键所产生的值是在运行时:JsonValuestringJsonValueundefinedJsonValueundefined

type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue | undefined };
//                             ^^^^^^^^^^^

这与 JavaScript 中 JSON 对象的序列化和反序列化算法兼容,因为它既不将属性序列化为值,也不支持 JSON 作为类型,因此永远不会存在于反序列化值中。undefinedundefinedundefined

此类型既方便(可以对任何属性名称使用点表示法属性访问),又是类型安全的(必须缩小每个值的范围才能安全地使用它)。JsonValue

TS Playground 中的完整代码


更新以回应您的评论

Playground 中的答案仍然取决于界面 Foo 是一种类型。但我需要它成为一个接口。这可能吗?

只有当接口扩展(受制于)类对象联合成员时,才有可能。JsonValue

TS 手册部分 Differences Between Type Aliases and Interfaces 以以下信息开头(重点是我的):

类型别名和接口非常相似,在许多情况下,您可以在它们之间自由选择。几乎所有的功能都可以在 中使用,关键的区别在于,不能重新打开类型来添加新属性,而接口始终是可扩展的。interfacetype

这意味着每个类型别名都是在定义它的站点上完成的......但是接口总是可以扩展(突变)的,所以在类型检查发生之前,确切的形状实际上不会最终确定,因为其他代码(例如,使用你的代码的其他代码)可以改变它。

防止这种情况的唯一方法是限制相关接口允许的扩展:

使用原始的、不受约束的接口的示例显示了编译器错误:

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 Playground 中的代码

另请参阅:TS 手册中的实用程序类型

评论

0赞 jsejcksn 12/6/2022
其他花絮:可以在 Deno 标准库存储库中查看与 JsonValue 类型相关的 PR 的搜索查询结果。惯用 TypeScript 使用 PascalCase 作为类型名称。
0赞 theahura 12/6/2022
Playground 中的答案仍然取决于界面 Foo 是一种类型。但我需要它成为一个接口。这可能吗?
0赞 jsejcksn 12/6/2022
^@theahura 这要视情况而定。我已经更新了答案,详细回答了您的问题。
1赞 Tobias S. 12/7/2022 #2

我最初在回答中的意思是放宽类型。你可以满足于这种类型。JSONValueobject

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采用某种类型并遍历其类型。它检查类型的属性,并将其解析为类型是否无效。Tnever

interface Foo { 
  name: 'FOO',
  fooProp: string,
  fn: () => void
}

type Validated = ValidateJSON<Foo>
// {
//     name: 'FOO';
//     fooProp: string;
//     fn: never;
// }

我们可以使用此实用程序类型来验证 内部的参数类型和返回类型。fnwrap

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 }
);

操场