charliebrownie
charliebrownie

Reputation: 6097

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:

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?

Upvotes: 3

Views: 4321

Answers (2)

charliebrownie
charliebrownie

Reputation: 6097

Solved this using Type Assertion, by just adding as FooAccount like this:

const fooAccount: FooAccount = findById(accountId) as FooAccount;

There's no need to modify the existing design to achieve it.

Basically, the assertion from type S to T succeeds if either S is a subtype of T or T is a subtype of S.

More info: https://basarat.gitbooks.io/typescript/docs/types/type-assertion.html

Upvotes: -3

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249606

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
}

Upvotes: 5

Related Questions