如何正确声明以下打字稿接口/类型?

问题描述

我正在用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之间建立链接。

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