Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: interface polymorphism issue

I have a base Account interface:

interface Account {
  id: number;
  email: string;
  password: string;
  type: AccountType;
}

where AccountType:

enum AccountType {
  Foo = 'foo',
  Bar = 'bar'
}

and two account subtypes (FooAccount and BarAccount) that extend the Account interface:

interface FooAccount extends Account {
  foo: Foo;
}
interface BarAccount extends Account {
  bar: Bar;
}

Account is an aggregate that holds basic account info and, depending on the type, owns a Foo or a Bar object.

Actions on these objects, can only be performed by their owners (the account).

I have defined an AccountRepository:

export interface AccountRepository {
  findById(accountId: number): Account;
}

where the findById(accountId: number) returns an Account, but this account could be any FooAccount or BarAccount.

I want to use this findById function before performing any action on a Foo or Bar. For example, let's say I want to update an account's Foo:

  • would use findById(accountId: number) to retrieve the account
  • check the AccountType of the account, in this case account.type === AccountType.Foo
  • if the AccountType check is correct, then would access account.foo.id and use that fooId to perform the desired update

The problem here is, that this last point is failing: as findById(accountId: number): Account returns an Account and there is no foo: Foo property defined in its interface.

I have also tried the following, but cannot be done either:

const fooAccount: FooAccount = findById(accountId);

because the function returns an Account.

I am trying to figure out how can this be achieved, what am I missing out? Is there anything I could be doing wrong?

like image 634
charliebrownie Avatar asked Jan 24 '26 03:01

charliebrownie


1 Answers

The best solution is probably to use a discriminated union.

export class Bar { public idBar: number; }
class Foo { public idFoo: number; }
interface AccountCommon {
  id: number;
  email: string;
  password: string;
}

enum AccountType {
  Foo = 'foo',
  Bar = 'bar'
}

interface FooAccount extends AccountCommon {
  type: AccountType.Foo; // type can only be Foo
  foo: Foo;
}
interface BarAccount extends AccountCommon {
  type: AccountType.Bar; // type can only be Bar
  bar: Bar;
}
// The discriminated union
type Account = BarAccount | FooAccount //type is common so type can be either Foo or Bar

export interface AccountRepository {
  findById(accountId: number): Account;
}

let r: AccountRepository;

let a = r.findById(0);
if (a.type === AccountType.Bar) { // type guard
  a.bar.idBar // a is now BarAccount
} else {
  a.foo.idFoo // a is now FooAccount
}
like image 69
Titian Cernicova-Dragomir Avatar answered Jan 26 '26 15:01

Titian Cernicova-Dragomir