如何在不初始化变量的情况下断言接口相等?

How to assert interface equality without initializing variables?

如何在不初始化任何值的情况下断言 2 个接口相同(duck-typing)?

我使用 graphql codegen 工具自动为 graphql 查询生成类型。 当我从不同的地方查询相同的数据时,会生成相同的界面。 我想通过在所有文件中导入相同的接口来巩固我的输入,同时确保接口的不同声明确实相同。

示例:

// src/components/__generated__/MyData.ts
export interface MyData_me {
  __typename: "User";
  id: string;
  name: string | null;
  avatarUrl: string | null;
  email: string | null;
}
// src/models/__generated__/UserData.ts
export interface UserData {
  __typename: "User";
  id: string;
  name: string | null;
  avatarUrl: string | null;
  email: string | null;
}
// src/models/User.ts
import { gql } from "@apollo/client";

import {MyData, MyData_me} from "../components/__generated__/MyData";
import {UserData} from "./__generated__/UserData";

export const UserDataFragment = gql`
  fragment UserData on User {
    id
    name
    avatarUrl
    email
  }
`

if (UserData !== MyData_me) { // [tsserver 2693] [E] 'UserData' only refers to a type, but is being used as a value here
  throw "We have a problem, interfaces TUser and MyData_me aren't identical!"
}
export type TUser = UserData;
export type TMe = MyData; 

这里有干净的解决方案吗?被迫用假数据初始化 2 个对象以进行测试 if (typeof user1 !== typeof user2) 听起来不对,因为它与我的应用程序逻辑无关。

如果你有两个运行时对象并比较它们是否相等(请注意,在大多数情况下检查 typeof user1 === typeof user2user1 === user2 不算作检查是否相等),它不会告诉你TypeScript 是否认为它们属于同一类型。例如:

const fruit = { apples: 5, oranges: 5 };

interface Apples {
    apples: number;
}
const apples: Apples = fruit; // okay

interface Oranges {
    oranges: number;
}
const oranges: Oranges = fruit; // okay

console.log(JSON.stringify(apples) === JSON.stringify(oranges)); // true
// but Apples and Oranges are not the same type

这里,applesoranges 在运行时是相同的,但根据编译器不同类型。仅仅因为 apples === oranges 并不意味着 ApplesOranges.


正如我在评论中提到的,到 JavaScript 是 运行 时,整个 TypeScript 静态类型系统,包括所有 interface 定义,已经 erased .运行时来不及捕获此类错误。

相反,您可能会出现编译错误。让我们定义一个名为 MutuallyExtends<T, U> 的通用类型别名,其中 TUconstrained to each other. (Strictly speaking this would be a prohibited circular constraint. Instead what we can do is introduce a new generic type parameter V that defaults to whatever is entered for T). It's not exactly true that if A extends B and if B extends A that A is the same type as B, but it's pretty close. There are stricter checks available,但相互扩展通常对我来说已经足够了。无论如何,这里是:

type MutuallyExtends<T extends U, U extends V, V = T> = any;

MutuallyExtends<T, U> 计算出的实际值只是 any 而不是我们真正关心的。关键是你不能写 MutuallyExtends<X, Y> 除非编译器认为 XY 可以相互赋值。

我们所要做的就是在类型位置的某处写入MutuallyExtends<MyData_me, UserData>,然后查看是否有错误。比如,这样说:

0 as MutuallyExtends<MyData_me, UserData>; // no error here

在运行时就变成了0;,一个无用的0求值。但重要的是编译器中没有类型错误。


对比一下如果你使用像

这样的错误类型会发生什么
interface MyData_Bad {
    _typename: "User"; // wrong number of underscores
    id: string;
    name: string | null;
    avatarUrl: string | null;
    email: string | null;
}

0 as MutuallyExtends<MyData_Bad, UserData>; // error!
// ----------------> ~~~~~~~~~~
// Type 'MyData_Bad' does not satisfy the constraint 'UserData'.
//   Property '__typename' is missing in type 'MyData_Bad' but 
//   required in type 'UserData'.

现在有一个错误告诉您 MyData_BadUserData 不兼容,甚至还提供了一些有关原因的信息。这大概会破坏您的构建(或者至少在构建过程中发出警告)并让您有机会在任何 JavaScript 代码运行之前做一些事情。


无论如何,希望能给你一些指导。祝你好运!

Playground link to code