I'm building an Apollo GraphQL server in Typescript and having trouble understanding the correct way to handle things within the type system. Although GraphQL and Apollo are a part of the code, I'm wondering specifically about the TypeScript part. I'm also having trouble understanding the role of interfaces vs types and what the best practice is for each (i.e., when do you use a type vs an interface, how do you handle extending each, etc).
Most of the unclarity I have is in the resolver. I'll leave comments and questions interspersed in the code next to the relevant parts that I'm asking about. Thank you again for any help you can offer:
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
}
}
}
My rule of thumb is to use
interfaces
where possible. Basically you can use an interface whenever you are dealing with an object that has known keys (the values can be complex types). So you can makeBankingAccount
aninterface
.The way you have set up the spending and savings accounts to both extend a shared interface is great!
When you have a
BankingAccount
, you know that it has spending or saving attributes, but you don't know which.One option is to check which type it is using a type guard.
The other option is to define an additional
type
which has all of the properties of both account types, but optional.What I personally would do is define your
BankingAccount
such that it must have complete attributes for either spending or saving, but it the other type's attributes are optional. That means you can access those properties without error, but they might beundefined
.And after typing all of this...I realized that
BankingAccount
has atype
. Is that type "spending" or "savings"? In that case, we also want to draw a link between thetype
and theattributes
.This
BankingAccount
type definition should allow you to access attributes of either type while also allowing you to narrow an account as either savings or spending just by checking the value oftype
. Because of the union here this has to be atype
rather than aninterface
, but that's not really consequential.Typescript Playground Link