What is the correct way to define the interfaces/types in typescript?

I am currently working on setting up an Apollo GraphQL server in Typescript and struggling with understanding the correct approach in dealing with the type system. While GraphQL and Apollo are integral to the code, my main focus is on TypeScript. I am also finding it challenging to grasp the distinction between interfaces and types, and determining the best practices for each (such as when to use a type versus an interface, how to handle extensions, etc).

Most of my confusion lies within the resolver function. I have included comments and questions within the code where I am seeking clarification. I appreciate any guidance you can provide:


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

// A question arises here about the correct way to define 'SpendingAccountAttributes | SavingsAccountAttributes' in order to convey that it can be one or the other. Would this union type only return shared fields between the two types, essentially those in 'BankingAttributes'?

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

// Should I eliminate 'SpendingAccountAttributes' and 'SavingsAccountAttributes' specific types and instead make them optional types within 'BankingAttributes'? I will eventually create resolvers for 'SpendingAccount' and 'SavingAccount' as separate queries, so having them may be useful. I am unsure 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. Is it better to stick with one or the other? If 'type', how can I extend 'BankingAttributes' to 'SpendingAccountAttributes' to indicate that they should be a part of the SpendingAccount's attributes?

export default {
  Query: {
    bankingAccounts: async(_source: string, _args: [], { dataSources}: Record<string, any>) : Promise<[BankingAccount]> => {
      // Here we are making a restful API call to an 'accounts' endpoint, passing in the type as '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,
          // The compilation fails when trying to access 'accountFeatures' with the error: 'accountFeatures does not exist on type 'SpendingAccountAttributes | SavingsAccountAttributes''
          // What is the best way to address this and retrieve 'accountFeatures' for the spending account (where this attribute is present)?
          accountFeatures,
          ...other
        }
      })

      return response
    }
  }
}

Answer №1

When dealing with objects that have known keys and complex value types, my preference is to use interfaces. This allows for greater flexibility and clarity in your code structure. For example, you can define the BankingAccount as an interface.

It's commendable that you have set up the spending and savings accounts to both extend a shared interface. This promotes reusability and consistency in your codebase.

By using a BankingAccount, you have access to attributes of both spending and savings accounts, but it may not be immediately clear which type it belongs to. To address this, consider implementing a type guard.

Another approach is to create a separate type that combines the properties of both account types as optional.

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

A suggested strategy would be to require complete attributes for one account type while allowing the attributes of the other type to be optional within the BankingAccount definition. This allows for error-free property access, even if they are sometimes undefined.

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

Furthermore, consider the connection between the account's type and its attributes. This linkage can provide additional context and enhance code clarity in the application.

With this refined BankingAccount definition, you can access attributes based on the account's type, allowing for precise type checking and property access.

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

For a hands-on experience, you can try playing around with this code in the Typescript Playground Link

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Out of the blue, I encountered an issue while trying to install Express in node.js using Types

Encountered a failure while attempting to install Express with node.js in Typescript. Received the following warning: https://i.sstatic.net/XcrGX.png Performed npm initialization, started index.js, created tsconfig.json, and installed ts-node. The comman ...

Validate certain elements within a form group in a wizard

Within my 2-step wizard, there is a form group in the first step. When the next page button is clicked on the first step, I want to validate the elements in that form group. My questions are: 1 - Would it be more effective to use 2 separate forms in each ...

Using string interpolation and fetching a random value from an enum: a comprehensive guide

My task is to create random offers with different attributes, one of which is the status of the offer as an enum. The status can be “NEW”, “FOR_SALE”, “SOLD”, “PAID”, “DELIVERED”, “CLOSED”, “EXPIRED”, or “WITHDRAWN”. I need ...

Can you share the appropriate tsconfig.json configuration for a service worker implementation?

Simply put: TypeScript's lib: ['DOM'] does not incorporate Service Worker types, despite @types/service_worker_api indicating otherwise. I have a functional TypeScript service worker. The only issue is that I need to use // @ts-nocheck at t ...

Dealing with a missing item in local storage in an Angular application

When working with local storage, I have a function that saves certain values and another method that reloads these values. However, what is the best approach to handle missing items in the local storage? This could happen if a user deletes an item or if it ...

Implementing Facebook Javascript SDK to enable login and trigger re-authentication using React Web and Typescript within a component

As a newcomer to stack overflow, I welcome any suggestions on how I can improve my question. I'm in need of guidance concerning logging a user into facebook and requiring them to authenticate their profile or select another profile manually, rather t ...

The ng-bootstrap typeahead is encountering an error: TypeError - Object(...) is not functioning correctly

Hey there! I'm trying to integrate the Angular Bootstrap typeahead component in my Angular 5 application by following the linkToTypeahead. However, I'm encountering some errors along the way. Here's what I'm seeing: ERROR TypeError: Ob ...

Leveraging enums within strictFunctionTypes for Typescript generics

Here is a code snippet (TS playground link): const enum Enum { A, B, C } interface Args { e: Enum.A; } interface GenericClass<A> { new (args: A) : void; } class TestClass { constructor(args: Args) {} } function func<A>(C: GenericCl ...

Counting up in Angular from a starting number of seconds on a timer

Is there a way to create a countup timer in Angular starting from a specific number of seconds? Also, I would like the format to be displayed as hh:mm:ss if possible. I attempted to accomplish this by utilizing the getAlarmDuration function within the tem ...

Where's the tsconfig.json for Firebase Emulators?

I've encountered an issue with my Firebase project that's written in JavaScript (not TypeScript). When attempting to run the functions emulator, I'm getting the following error: $ firebase emulators:start --only functions ⚠ functions: Ca ...

Identifying Shifts in Objects Using Angular 5

Is there a way to detect changes in an object connected to a large form? My goal is to display save/cancel buttons at the bottom of the page whenever a user makes changes to the input. One approach I considered was creating a copy of the object and using ...

Creating non-changing identifiers with ever-changing values in Angular by leveraging TypeScript?

I need to transform all the labels in my application into label constants. Some parts of the HTML contain dynamic content, such as This label has '1' dynamic values, where the '1' can vary based on the component or a different applicat ...

Retrieve an array of object values using Angular and TypeScript

I'm attempting to extract the values of objects in an array using a nested for loop. I am receiving JSON data as shown below and have written the following TypeScript code to achieve this. However, I am unable to successfully bind the values to the te ...

Is there a way to bypass the "Error: Another application is currently displaying over Chrome" message using Javascript or Typescript?

Can the "Another app is displaying over chrome error" be bypassed using JavaScript or TypeScript? Error Message: https://i.stack.imgur.com/iSEuk.png ...

Typescript - Interface containing both mandatory and optional properties of the same type

Looking for a solution where an interface consists of a fixed property and an optional property, both being of type string. export interface Test{ [index: string]: { 'list': string[]; // <<< throws TS2411 error [in ...

Having difficulty in synchronizing the redux state and realm database into harmony

Struggling to update my redux store with data from useState. While troubleshooting, I noticed that errors are often related to the realm database, impacting the redux store unintentionally. LOG [Error: Wrong transactional state (no active transaction, wr ...

Activate the function only once the display has finished rendering all items from ng-repeat, not just when ng-repeat reaches its last index

Currently, I am generating a list using ng-repeat and each iteration is rendering a component tag with a unique id based on the $index value. The implementation looks like this: <div ng-if="$ctrl.myArr.length > 0" ng-repeat="obj in $ctrl.myArr"> ...

Issue encountered while implementing a recursive type within a function

I've created a type that recursively extracts indices from nested objects and organizes them into a flat, strongly-typed tuple as shown below: type NestedRecord = Record<string, any> type RecursiveGetIndex< TRecord extends NestedRecord, ...

Replace i18next property type in React for language setting

We have decided to implement multilanguage support in our app and encountered an issue with function execution. const someFunction = (lang: string, url: string) => any If we mistakenly execute the function like this: someFunction('/some/url', ...

"Null value is no longer associated with the object property once it has

What causes the type of y to change to string only after the destruction of the object? const obj: { x: string; y: string | null } = {} as any const { x, y } = obj // y is string now ...