kibe
kibe

Reputation: 303

How to match types in Typescript?

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

Answers (3)

Radim Göth
Radim Göth

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

Linda Paiste
Linda Paiste

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

Typescript Playground Link

Upvotes: 7

Teneff
Teneff

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

Related Questions