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

yyhrrdl8  于 2022-12-15  发布在  TypeScript
关注(0)|答案(2)|浏览(196)

这是Typescript: passing interface as parameter for a function that expects a JSON type的扩展(询问如何将接口传递给JSON类型的函数),而Typescript: passing interface as parameter for a function that expects a JSON type又是Typescript: interface that extends a JSON type的扩展(询问如何在JSON类型之间转换)
这些问题与JSON Typescript类型相关:

type JSONValue = 
 | string
 | number
 | boolean
 | null
 | JSONValue[]
 | {[key: string]: JSONValue}

Typescript: passing interface as parameter for a function that expects 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,即使通过分析很容易识别出转换应该很好。
请参见Playground以及https://github.com/microsoft/TypeScript/issues/15300
先前的答复指出:
我们在不扩展JSONValue类型的情况下的唯一解决方法是将[interface] Foo转换为一个类型。
在我的例子中,我可以修改JSONValue类型,但是不能轻易地修改所有相关的接口。

bbmckpt7

bbmckpt71#

我最初的意思是放松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;
// }

我们可以使用这个实用程序类型来验证wrapfn的参数类型和返回类型。

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

Playground

yduiuuwa

yduiuuwa2#

如果您使用一个单独的泛型作为返回类型(它也扩展了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对象的serializationdeserialization算法兼容,因为它既不序列化以undefined作为值的属性,也不支持以undefined作为类型,因此undefined永远不会存在于反序列化的值中。
这种类型既方便(您可以对任何属性名使用点标记属性访问),又是类型安全的(您必须将每个值限制为JsonValue才能安全地使用它)。
TS Playground中的完整代码
根据您的意见更新:
操场上的答案仍然依赖于接口Foo是一个类型。2但是我需要它是一个接口。3这可能吗?
只有当接口扩展JsonValue的类对象联合成员(受其约束)时,才有可能。
TS手册的类型别名和接口之间的区别一节以下面的信息开始(重点是我):
类型别名和接口非常相似,在很多情况下你可以自由选择它们。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 👍

TSPlayground代码
另请参见TS手册中的utility types

相关问题