I'm working on a financial app that has "Bids" (offers to buy something) and "Asks" (offers to sell something).
interface Ask {
price: number
amount: number
}
interface Bid {
price: number
amount: number
}
How can I avoid repeating two identical type definitions? (They will always be identical)
I could have only one interface and call it Offer
but it loses meaning and clarity.
I tried this:
interface Offer {
price: number
amount: number
}
interface Bid extends Offer {}
interface Ask extends Offer {}
But I get an eslint error saying An interface declaring no members is equivalent to its supertype.
and there is no point extending the interface if I don't add something to it.
You can use a type alias:
interface Ask {
price: number;
amount: number;
}
type Bid = Ask;
Note that because TypeScript's type system is structural (based on the shape of types), not nominative (based on the names/identities of types), objects with the type Ask
will be assignable to Bid
variables/properties/parameters and vice-versa. TypeScript doesn't distinguish between them:
let a: Ask = { price: 42, amount: 2 };
// ^? −−− let a: Ask
let b: Bid = { price: 67, amount: 4 };
// ^? −−− let b: Ask
a = b; // <== This is allowed
Note that the type hint shown for b
is Ask
, not Bid
.
If you want to maintain a distinction between the two types, you can use a technique described by Drew Colthorp in the article Flavoring: Flexible Nominal Typing for TypeScript (thank you VLAZ for pointing that out; see also VLAZ's answer to a related question). The core of it is that you can differentiate the types by using an optional branding property, like this (but keep reading):
interface Offer {
price: number;
amount: number;
}
interface Bid extends Offer {
__type__?: "Bid";
}
interface Ask extends Offer {
__type__?: "Ask";
}
Since the property is optional, you don't have to specify it when creating instances, but it makes the types incompatible with each other (though you can easily convert from one to the other):
let a: Ask = { price: 42, amount: 2 };
// ^? −−− let a: Ask
let b: Bid = { price: 67, amount: 4 };
// ^? −−− let b: Bid
a = b; // <== Error: Type 'Bid' is not assignable to type 'Ask'.
// Types of property '__type__' are incompatible.
// Type '"Bid" | undefined' is not assignable to type '"Ask" | undefined'.(2322)
In the article, Colthorp takes it further by creating a reusable generic Flavor
type:
interface Flavoring<FlavorT> {
_type?: FlavorT;
}
type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;
which you'd use in your code like this:
interface Offer {
price: number;
amount: number;
}
type Bid = Flavor<Offer, "Bid">;
type Ask = Flavor<Offer, "Ask">;
The same tests with a
and b
show that you can't assign the one to the other.
Finally, you could use fully-branded types by actually including the _type
(or __type__
) property in the instances and not making them optional. I find it handy to do that sometimes because I can see those properties in the debugger and I can use them for type narrowing in code that works with Offer
instances. But sometimes you don't want the extra properties cluttering up the objects, hence the "flavor" approach above. Example of full branding:
interface Offer {
price: number;
amount: number;
}
interface Bid extends Offer {
__type__: "Bid";
}
interface Ask extends Offer {
__type__: "Ask";
}
let a: Ask = { price: 42, amount: 2, __type__: "Ask" };
// ^? −−− let a: Ask
let b: Bid = { price: 67, amount: 4, __type__: "Bid" };
// ^? −−− let b: Bid
a = b; // <== Error: Type 'Bid' is not assignable to type 'Ask'.
// Types of property '__type__' are incompatible.
// Type '"Bid"' is not assignable to type '"Ask"'.(2322)