I've been writing TypeScript without understanding it -- pt. 2
Tags: typescript, tutorial, learning
Published: 2024-07-09 14:14:00

## Give me a Break. I'm still learning!

Hey everyone. I'm back.

And, yeah, I'm still making n00b TypeScript mistakes 😢

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gwsl461eo3v3nnohpi6c.png)

But luckily I've got some really clever coworkers that point out some awesome TypeScript tips while I continue to build [Open SaaS](https://OpenSaaS.sh) and make it the best, free, open-source SaaS starter for React & NodeJS.

[![https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf1fhsgwuurkre9a7drq.png)](https://OpenSaaS.sh)

And I'm going to share those tips with you today.

In the [first part of this series on TypeScript](https://dev.to/wasp/ive-been-writing-typescript-without-understanding-it-5ef4), I went into some fundamentals on what it is and how it works. I also touched on the `satisfies` keyword and some of the quirks around TypeScript's structural typing system.

In this episode, I'm going to teach you how to share a set of values across a large app (such as a SaaS app) by using a nifty technique to make sure that you never forget to update other parts of your app whenever new values are added or changed.

Let's jump right into some code then.

## Keeping track of values across a big app

In [Open SaaS](http://opensaas.sh), we wanted to assign some payment plan values we could use across the entire app, both front-end and back-end. For example, most SaaS apps have a few different products plans they might sell, like:
  - a monthly `Hobby` subscription plan,
  - a monthly `Pro` subscription plan,
  - and a one-time payment product that gives the user 10 `Credits` they can redeem in the app (instead of a monthly plan).

So it seemed like a good idea to use an `enum` and to pass those plan values around and keep them consistent:

```tsx
export enum PaymentPlanId {
  Hobby = 'hobby',
  Pro = 'pro',
  Credits10 = 'credits10',
}
```

Then, we could use this enum on the Pricing Page, as well as in our server-side functions.

```tsx
// ./client/PricingPage.tsx

import { PaymentPlanId } from '../payments/plans.ts'

export const planCards = [
  {
    name: 'Hobby',
    id: PaymentPlanId.Hobby,
    price: '$9.99',
    description: 'All you need to get started',
    features: ['Limited monthly usage', 'Basic support'],
  },
  {
    name: 'Pro',
    id: PaymentPlanId.Pro,
    price: '$19.99',
    description: 'Our most popular plan',
    features: ['Unlimited monthly usage', 'Priority customer support'],
  },
  {
    name: '10 Credits',
    id: PaymentPlanId.Credits10,
    price: '$9.99',
    description: 'One-time purchase of 10 credits for your account',
    features: ['Use credits for e.g. OpenAI API calls', 'No expiration date'],
  },
];

export function PricingPage(props) {
  return (
    ...
      planCards.map(planCard => {
        <PlanCard card={planCard} />
      })
    ...
  )
}
```

Above, you can see how we use the enum as the payment plan ID on our Pricing Page. Then, we pass that ID to our button click handler, and send it in our request to the server so we know which payment plan to process.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ypfmqqniu62oy8ijfv9.png)

```tsx
// ./server/Payments.ts

export const stripePayment: StripePayment<PaymentPlanId, StripePaymentResult> = async (plan, context) => {
  let stripePriceId;
  if (plan === PaymentPlanId.Hobby) {
    stripePriceId = process.env.STRIPE_HOBBY_SUBSCRIPTION_PRICE_ID!;
  } else if (plan === PaymentPlanId.Pro) {
    stripePriceId = process.env.STRIPE_PRO_SUBSCRIPTION_PRICE_ID!;
  } else if (plan === PaymentPlanId.Credits10) {
    stripePriceId = process.env.STRIPE_CREDITS_PRICE_ID!;
  } else {
    throw new HttpError(404, 'Invalid plan');
  }
  
  //...
```

The nice thing about using the enum here is that it’s easy to use consistently across the entire app. And in the case above, we use it to map our pricing plans to the price IDs that they were given when we created these products on Stripe, which we've saved as environment variables. 

But with our current code, what happens if we decide to create a new plan, such as a 50 Credit one-time payment plan, and add it to our app?

```tsx
export enum PaymentPlanId {
  Hobby = 'hobby',
  Pro = 'pro',
  Credits10 = 'credits10',
  Credits50 = 'credits50'
}
```

Well, currently, we’d have to go through the app, find each place where we’re using `PaymentPlanID`, and add a reference to our new `Credits50` plan.

```tsx
// ./client/PricingPage.tsx

import { PaymentPlanId } from '../payments/plans.ts'

export const planCards = [
  {
    name: 'Hobby',
    id: PaymentPlanId.Hobby,
    //...
  },
  {
    name: 'Pro',
    id: PaymentPlanId.Pro,
    price: '$19.99',
    //...
  },
  {
    name: '10 Credits',
    id: PaymentPlanId.Credits10,
    //...
  },
  {
    name: '50 Credits',
    id: PaymentPlanId.Credits50.
    //...
  }
];

export function PricingPage(props) {
  return (
    ...
      planCards.map(planCard => {
        <PlanCard card={planCard} />
      })
    ...
  )
}

// ./server/Payments.ts

export const stripePayment: StripePayment<PaymentPlanId, StripePaymentResult> = async (plan, context) => {
  let stripePriceId;
  if (plan === PaymentPlanId.Hobby) {
    stripePriceId = process.env.STRIPE_HOBBY_SUBSCRIPTION_PRICE_ID!;
  } else if (plan === PaymentPlanId.Pro) {
    //..
  } else if (plan === PaymentPlanId.Credits50) {
    stripePriceId = process.env.STRIPE_CREDITS_50_PRICE_ID!; // ✅
  } else {
    throw new HttpError(404, 'Invalid plan');
  }
```

Ok. That might not seem too difficult, but what if you’re using `PaymentPlanId` in more than just two files? There is a really high chance you will forget to reference your new payment plan somewhere!

Wouldn’t it be cool if we could have TypeScript tell us when we forgot to add it somewhere? This is exactly the problem the `Record` type can help us solve. 

Let’s take a look.

## Using Record Types to Keep Values in Sync

First off, a `Record` is a utility type to help us type objects. By using `Record` we can define exactly what types our keys and values should be.

The type `Record<X, Y>` on an object means "This object literal must define a _value of type Y for every possible value of type X_". In other words, Records enforce compile-time checks for exhaustiveness.

In practical terms, what this means is that when someone adds a new value to the enum `PaymentPlanId`, the compiler won't let them forget about adding an appropriate mapping

This keeps our object mappings strong and safe.

Let's take a look at how it works with our `PaymentPlanId` enum in action. Let’s first look at how we could use a `Record` type to make sure we always have all the Payment Plans included on our pricing page:

```tsx
export enum PaymentPlanId {
  Hobby = 'hobby',
  Pro = 'pro',
  Credits10 = 'credits10',
}

// ./client/PricingPage.tsx

export const planCards: Record<PaymentPlanId, PaymentPlanCard> = {
  [PaymentPlanId.Hobby]: {
    name: 'Hobby',
    price: '$9.99',
    description: 'All you need to get started',
    features: ['Limited monthly usage', 'Basic support'],
  },
  [PaymentPlanId.Pro]: {
    name: 'Pro',
    price: '$19.99',
    description: 'Our most popular plan',
    features: ['Unlimited monthly usage', 'Priority customer support'],
  },
  [PaymentPlanId.Credits10]: {
    name: '10 Credits',
    price: '$9.99',
    description: 'One-time purchase of 10 credits for your account',
    features: ['Use credits for e.g. OpenAI API calls', 'No expiration date'],
  }
};

export function PricingPage(props) {
  return (
    ...
      planCards.map(planCard => {
        <PlanCard card={planCard} />
      })
    ...
  )
}
```

Now `planCards` is a `Record` type where the key has to be a `PaymentPlanId`, and the value must be an object with the payment plan information (`PaymentPlanCard`).

The magic here happens when we add a new value to our enum, such as `Credits50`:

```tsx
export enum PaymentPlanId {
  Hobby = 'hobby',
  Pro = 'pro',
  Credits10 = 'credits10',
  Credits50 = 'credits50'
}
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ap8u3evb5qytjdfatnm.png)

Now TypeScript is giving us a compile-time error, `Property '[PaymentPlanId.Credits50]' is missing...`, to let us know that our Pricing Page doesn’t contain a card for our new plan.

Now you see the simple power of using a `Record` to keep values consistent. But we shouldn’t only do this for the front-end, let’s fix our server-side function that process payments for our different plans:

```tsx
// ./payments/plans.ts
export const paymentPlans: Record<PaymentPlanId, PaymentPlan> = {
  [PaymentPlanId.Hobby]: {
    stripePriceId: process.env.STRIPE_HOBBY_SUBSCRIPTION_PRICE_ID,
    kind: 'subscription'
  },
  [PaymentPlanId.Pro]: {
    stripePriceId: process.env.STRIPE_PRO_SUBSCRIPTION_PRICE_ID,
    kind: 'subscription'
  },
  [PaymentPlanId.Credits10]: {
    stripePriceId: process.env.STRIPE_CREDITS_PRICE_ID,
    kind: 'credits', 
    amount: 10
  },
  [PaymentPlanId.Credits50]: {
    stripePriceId: process.env.STRIPE_CREDITS_PRICE_ID,
    kind: 'credits', 
    amount: 50
  },
};

// ./server/Payments.ts
import { paymentPlans } from './payments/plans.ts'

export const stripePayment: StripePayment<PaymentPlanId, StripePaymentResult> = async (planId, context) => {
  const priceId = paymentPlans[planId].stripePriceId
  
  //...
```

What’s really cool about this technique, is that by defining `paymentPlans` with a `Record` type that uses our `PaymentPlanId` enum as the key value, we can always be sure we never forget any of our payment plans or make a silly typo. TypeScript will save us here. 

Plus, we can switch out our entire `if else` block for a clean one-liner: 

```tsx
const priceId = paymentPlans[planId].stripePriceId
```

Smoooooth :)

It’s also very likely that we will use the `paymentPlans` object elsewhere in our code, making it cleaner and much more maintainable. A real win-win-win situation, thanks to the `Record` type.

## Preferring Mappings with `Record` over `if else`

Just to further bring the point home of how `Record` can make our lives easier as developers, let’s look at another example of using it client-side to display some user account information.

First, let’s summarize what’s going on in our app and how we used our friendly utility type already:

1. we defined the `PaymentPlanId` enum to centralize our payment plan IDs and keep them consistent across the app.
2. we mapped objects using `Record` in the client and server code to make sure that all our Payment Plans are present in these objects, that way if we add a new payment plan we will get TypeScript warnings that they must also be added to these objects as well.

Now, we use those IDs on the front-end and pass them to our server-side call to process the payment for the correct plan when a user clicks the `Buy Plan` button. When the user completes the payment, we save that `PaymentPlanId` to a property on the user model in our database, e.g. `user.paymentPlan`.

Let’s now take a look at how we can again use that value, along with objects mapped with the `Record` type, to conditionally retrieve account information in a way that’s much cleaner and more type-safe than `if else` or `switch` blocks can be:

```tsx
// ./client/AccountPage.tsx

export function AccountPage({ user }: { user: User }) {
  const paymentPlanIdToInfo: Record<PaymentPlanId, string> = {
    [PaymentPlanId.Hobby]: 'You are subscribed to the monthly Hobby plan.',
    [PaymentPlanId.Pro]: 'You are subscribed to the monthly Pro plan.',
    [PaymentPlanId.Credits10]: `You purchased the 10 Credits plan and have ${user.credits} left`,
    [PaymentPlanId.Credits50]: `You purchased the 50 Credits plan and have ${user.credits} left`
  };
  
  return (
    <div>{ paymentPlanIdToInfo[user.paymentPlan] }</div>
  )
}
```

Again, all we have to do is update our `PaymentPlanId` enum to include any additional payment plans we may create, and TypeScript will warn us that we need to add it to all mappings where it was used as a `Record` key or value type.

In comparison, if we were using an `if else` block, we’d get no such warnings. We’d also have no protection against silly typos, leading to buggier, harder to maintain code:

```tsx
export function AccountPage({ user }: { user: User }) {
  let infoMessage = '';

  if(user.paymentPlan === PaymentPlanId.Hobby) {
    infoMessage = 'You are subscribed to the monthly Hobby plan.';

  // ❌ We forgot the Pro plan here, but will get no warning from TS!

  } else if(user.paymentPlan === PaymentPlanId.Credits10) { 
    infoMessage = `You purchased the 10 Credits plan and have ${user.credits} left`;

  // ❌ Below we used the wrong user property to compare to PaymentPlanId.
  // Although it's acceptable code, it's not the correct type!
  } else if(user.paymentStatus === PaymentPlanId.Credits50) {
    infoMessage = `You purchased the 50 Credits plan and have ${user.credits} left`;
  }

  return (
    <div>{ infoMessage }</div>
  )
}
```

But there are times when we need more complex condition checking and the ability to handle any side cases individually. In such cases, we’re definitely better off using `if else` or `switch` statements. 

So how can we get the same type-checking thoroughness of `Record` mappings, but with the benefits of an `if else` or `switch`?

---
**By the way…**

We're working hard at [Wasp](https://wasp-lang.dev/) to create the best open-source React/NodeJS framework that allows you to move fast!

That's why we've got ready-to-use full-stack app templates with a simple CLI command, like [Open SaaS](https://opensaas.sh), or a ToDo App with TypeScript. All you have to do is install Wasp:

```sh
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
```

and run:

```sh
wasp new -t saas
# or 
wasp new -t todo-ts
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jj26gymrxyybut4541lg.png)

You'll get a full-stack templates with Auth and end-to-end TypeSafety, out of the box, to help you learn TypeScript, or to get  you started building your profitable side-project quickly and safely :)

---

### Using `never`… sometimes

The answer to the above question is that we need a way to check for “exhaustiveness” in our `switch` statements. Let’s use the example below:

```tsx
  // ./payments/Stripe.ts
  
  const plan = paymentPlans[planId];

  let subscriptionPlan: PaymentPlanId | undefined;
  let numOfCreditsPurchased: number | undefined;
  
  switch (plan.kind) {
    case 'subscription':
      subscriptionPlan = planId;
      break;
    case 'credits':
      numOfCreditsPurchased = plan.effect.amount;
      break;
  } 
```

We’ve reached for a relatively simple `switch` statement here instead of a mapping with the `Record` type because assigning the values of our two variables, `subscriptionPlan` and `numOfCreditsPurchased` , is a lot cleaner and easier to read this way.

But now we’ve lost the exhaustive type checking we’d get with a `Record` type mapping, so if we were to add a new `plan.kind`, like `metered-usage` for example, we’d get no warning from TypeScript in our `switch` statement above.

Boo!

Luckily, there is an easy solution. We can create a utility function that will do the checking for us:

```tsx
export function assertUnreachable(x: never): never {
  throw Error('This code should be unreachable');
}
```

This might look weird, but what’s important is the use of the `never` type. It tells TypeScript that this value should “never” occur. 

So that we can see how this utility function works, let’s go ahead now and add our new plan `kind`:

```tsx
// ./payments/plans.ts
export const paymentPlans: Record<PaymentPlanId, PaymentPlan> = {
  [PaymentPlanId.Hobby]: {
    stripePriceId: process.env.STRIPE_HOBBY_SUBSCRIPTION_PRICE_ID,
    kind: 'subscription'
  },
  [PaymentPlanId.Pro]: {
    stripePriceId: process.env.STRIPE_PRO_SUBSCRIPTION_PRICE_ID,
    kind: 'subscription'
  },
  [PaymentPlanId.Credits10]: {
    stripePriceId: process.env.STRIPE_CREDITS_PRICE_ID,
    kind: 'credits', 
    amount: 10
  },
  // ✅ Our new payment plan kind
  [PaymentPlanId.MeteredUsage]: {
    stripePriceId: process.env.STRIPE_METERED_PRICE_ID,
    kind: 'metered-usage'
};
```

Now, if we add `assertUnreachable`, check out what happens:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwlfr1dvdt23sz7lkzu8.png)

Ah ha! We’re getting an error `Argument of type '{ kind: "metered-usage"; }' is not assignable to parameter of type 'never'`

Perfect. We’ve introduced exhaustive type checking into our `switch` statement. This code is actually never meant to be run, it’s just there to provide friendly warnings for us in advance.

To get TypeScript to stop being mad at us in this case, all we have to do is…:

```tsx
  switch (plan.kind) {
    case 'subscription':
      subscriptionPlan = planId;
      break;
    case 'credits':
      numOfCreditsPurchased = plan.effect.amount;
      break;
    // ✅ Add our new payment plan kind
    case 'metered-usage'
      currentUsage = getUserCurrentUsage(user);
      break;
    default:
      assertUnreachable(plan.kind);
  } 
```

This is great. We get all the benefits of dealing with more complex logic in a `switch` statement, with the assurance that we'll never forget any possible `plan.kind` case being used in our app.

Stuff like this makes code way less error prone, and much easier to debug. A little bit of preparation goes a long way!

## Continuing the TypeScript Tales

That was part 2 of this series, “I’ve been using TypeScript without understanding it” where I share my journey in learning the finer points of TypeScript from friends and colleagues as I build and maintain [Open SaaS,](https://opensaas.sh) an entirely free, open-source SaaS starter template.

I’m trying my best to make [Open SaaS](https://opensaas.sh) as professional and full-featured as possible, without making it too complicated, and to share what I learn in the process in an easy-going way. If you find anything about this process confusing, let us know in the comments and we’ll clarify the best we can. 

Also, if you like what we’re doing here, either with the articles or with [Open SaaS](https://opensaas.sh), please let us know and consider [giving us a star on GitHub](https://github.com/wasp-lang/wasp) ! It helps to motivate us and bring you more of this stuff.

Thanks, and see you in the next article.