Reputation: 303
In F#, you can do this:
type DeliveredOrderData =
{
OrderId: int;
DateDelivered: DateTime;
}
type UndeliveredOrderData =
{
OrderId: int;
}
type Order =
| Delivered of DeliveredOrderData
| Undelivered of UndeliveredOrderData
and then I can create functions that return depending on the state:
let putOnTruck order =
match order with
| Undelivered {OrderId=id} ->
OutForDelivery {OrderId=id}
| Delivered _ ->
failwith "package already delivered"
I get how to create types in TypeScript, but how can I do the same as above?
const putOrderOnTruck = (order: UndeliveredOrder) => {
// how can I make sure order is really UndeliveredOrder?
}
Upvotes: 13
Views: 13684
Reputation: 177
Pattern matching is not an integral part of Typescript but you can substitute it with package ts-pattern.
import { match, P } from "ts-pattern";
type Order =
| { status: 'delivered'; orderId: number; dateDelivered: string }
| { status: 'undelivered'; orderId: number; };
const putOnTruck = (order: Order) => {
match(order)
.with({status: 'delivered', dateDelivered: P.select()}, (dateDelivered, order) =>
console.log(`Order ${order.orderId} has been delivered at ${dateDelivered}`))
.with({status: 'undelivered'}, (order) =>
console.log(`Order ${order.orderId} has not been delivered yet`))
.exhaustive();
}
Upvotes: 1
Reputation: 42188
The two main typescript concepts at play here are Discriminating Unions and Type Guards.
We have two different types of order data. The difference is that one has a DateDelivered
and the other doesn't. We can make that very explicit by saying that UndeliveredOrderData
can never
have a DateDelivered
(ie. this property must not exist, or be set to undefined
).
type DeliveredOrderData = {
OrderId: number;
DateDelivered: number;
}
type UndeliveredOrderData = {
OrderId: number;
DateDelivered?: never;
}
type Order = DeliveredOrderData | UndeliveredOrderData
If we have an Order
which could be either of the two types, we can tell which type it is by seeing if there is a DateDelivered
or not. We put that logic into a user-defined type guard which tells typescript to narrow the type based on the result.
const isDelivered = (order: Order): order is DeliveredOrderData => {
return !! order.DateDelivered;
}
Let's say we have a function that requires an UndeliveredOrderData
const doPutOnTruck = (order: UndeliveredOrderData) => {
}
But at runtime, we aren't sure if an Order
is delivered or not. We can use our type guard inside an if
statement and act differently in the two branches.
const maybePutOnTruck = (order: Order) => {
if ( isDelivered( order ) ) {
throw new Error("package already delivered");
}
// type of `order` is now `UndeliveredOrderData`
doPutOnTruck(order);
}
Upvotes: 7
Reputation: 32158
TypeScript's types can only be used for static type checking, so in order to do that you should check the object's properties.
Here is an example
enum OrderType {
Delivered,
Undelivered
}
type DeliveredOrderData = {
type: OrderType
OrderId: number;
DateDelivered: Date
}
type UndeliveredOrderData = {
type: OrderType
OrderId: number;
}
function OutForDelivery(order: UndeliveredOrderData) {
}
let putOnTruck = (orderData: DeliveredOrderData | UndeliveredOrderData) => {
switch(orderData.type) {
case OrderType.Undelivered:
return OutForDelivery(orderData);
case OrderType.Delivered:
throw new Error('package already delivered');
}
}
Upvotes: 4