如何正确声明以下打字稿 interfaces/types?

How do I declare the following typescript interfaces/types correctly?

我正在使用 Typescript 构建 Apollo GraphQL 服务器,但无法理解在类型系统中处理事物的正确方法。尽管 GraphQL 和 Apollo 是代码的一部分,但我特别想知道 TypeScript 部分。我也无法理解接口与类型的作用以及每种接口的最佳实践(即,何时使用类型与接口,如何处理扩展等)。

我的大部分不明之处都在解析器中。我将在我要询问的相关部分旁边的代码中留下评论和问题。再次感谢您提供的任何帮助:


type BankingAccount = {
  id: string;
  type: string;
  attributes: SpendingAccountAttributes | SavingsAccountAttributes
}

// I've realized this "SpendingAccountAttributes | SavingsAccountAttributes" is the wrong way
// to do what I'm trying to do. I essentially want to the tell the 
// type system that this can be one or the other. As I understand it, the way it is written
// will create a Union, returning only the fields that are shared between both types, in this
// case what is essentially in the `BankingAttributes` type. Is that correct?

interface BankingAttributes = {
  routingNumber: string;
  accountNumber: string;
  balance: number;
  fundsAvailable: number;
}

// Is it better to remove the `SpendingAccountAttributes` and `SavingsAccountAttribute` specific
// types and just leave them as optional types on the `BankingAttributes`. I will
// in time be creating a resolver for the `SpendingAccount` and `SavingAccount` as standalone
// queries so it seems useful to have them. Not sure though


interface SpendingAccountAttributes extends BankingAttributes {
  defaultPaymentCardId: string;
  defaultPaymentCardLastFour: string;
  accountFeatures: Record<string, unknown>;
}

interface SavingsAccountAttributes extends BankingAttributes {
  interestRate: number;
  interestRateYTD: number;
}

// Mixing types and interfaces seems messy. Which one should it be? And if "type", how can I
// extend the "BankingAttributes" to "SpendingAccountAttributes" to tell the type system that
// those should be a part of the SpendingAccount's attributes?


export default {
  Query: {
    bankingAccounts: async(_source: string, _args: [], { dataSources}: Record<string, any>) : Promise<[BankingAccount]> => {
      // The following makes a restful API to an `accounts` api route where we pass in the type as an `includes`, i.e. `api/v2/accounts?types[]=spending&types[]=savings
      const accounts = await.dataSources.api.getAccounts(['spending', 'savings'])

      const response = accounts.data.map((acc: BankingAccount) => {
        const { fundsAvailable, accountFeatures, ...other } = acc.attributes

        return {
          id: acc.id,
          type: acc.type,
          balanceAvailableForWithdrawal: fundsAvailable,
          // accountFeatures fails the compilation with the following error:
          // "accountFeatures does not exist on type 'SpendingAccountAttributes | SavingsAccountAttributes'
          // What's the best way to handle this so that I can pull the accountFeatures
          // for the spending account (which is the only type of account this attribute will be present for)?
          accountFeatures,
          ...other
        }
      })

      return response
    }
  }
}

我的经验法则是尽可能使用 interfaces。基本上,只要处理具有已知键(值可以是复杂类型)的对象,就可以使用接口。所以你可以让 BankingAccount 成为 interface.

您设置支出和储蓄账户以同时扩展共享界面的方式很棒!

当你有一个 BankingAccount 时,你知道它有消费或储蓄属性,但你不知道是哪个。

一个选项是使用 type guard.

检查它使用的是哪种类型

另一种选择是定义一个额外的 type,它具有两种帐户类型的所有属性,但可选。

type CombinedAttributes = Partial<SpendingAccountAttributes> & Partial<SavingsAccountAttributes>

我个人会做的是定义您的 BankingAccount,使其必须具有用于支出或储蓄的完整属性,但其他类型的属性是可选的。这意味着您可以毫无错误地访问这些属性,但它们可能是 undefined.

interface BankingAccount = {
  id: string;
  type: string;
  attributes: (SpendingAccountAttributes | SavingsAccountAttributes) & CombinedAttributes 
}

输入所有这些后...我意识到 BankingAccount 有一个 type。那种类型是“支出”还是“储蓄”?在那种情况下,我们还想在 typeattributes 之间绘制一个 link。

BankingAccount 类型定义应允许您访问任一类型的属性,同时还允许您仅通过检查 type 的值来将帐户缩小为储蓄或支出。由于这里的联合,这必须是 type 而不是 interface,但这并不是真正重要的。

type BankingAccount = {
    id: string;
    attributes: CombinedAttributes;
} & ({
    type: "savings";
    attributes: SavingsAccountAtrributes;
} | {
    type: "spending";
    attributes: SpendingAccountAttributes;
}) 

function myFunc( account: BankingAccount ) {

    // interestRate might be undefined because we haven't narrowed the type
    const interestRate: number | undefined = account.attributes.interestRate;

    if ( account.type === "savings" ) {
        // interestRate is known to be a number on a savings account
        const rate: number = account.attributes.interestRate
    }
}

Typescript Playground Link