# Typescript Tapas

TypeScript is an incredible tool, but to fully benefit from its capabilities, you need to be aware of its full potential. Writing well-typed code is a superpower in itself – sometimes, with just minor changes to your types, you can significantly enhance the safety and maintainability of your code.
We've compiled some real-life examples to demonstrate how code can perform better with improved typing. These code snippets may be small, but tasty – much like some good tapas.

- URL: https://sudolabs.com/insights/typescript-tapas
- Published: 2024-04-17
- Updated: 2024-04-17
- Author: Oliver Dendis, Software Engineer
- Categories: Tech
- Language: English

In this article, we will show practical applications of template literal types, effective utilization of type predicates, and tips for type narrowing for consistently obtaining accurate types.

## 1. Don’t hide any rules to typescript

When working with React or Next.js routers, I've frequently noticed the convenience of storing route names and paths in constants or enums. Typically, it appears like this:

```typescript
const PageRoutes = {
	Home: "/",
	SignIn: "/sign-in",
	Error: "/error"
}

// ...later in code
const navigateToHomePage = () => {
	router.push(PageRoutes.Home)
}
```

It seems straightforward, as TypeScript is capable of inferring the type of routes on its own. However, in the code above, there are some implicit rules:

- All paths must start with "/".
- All keys of the PageRoutes object start with a capitalized letter.

Therefore, if someone (and yes, it could even be you!) breaks these rules, there won't be any complaints.

```typescript
const PageRoutes = {
	Home: "/",
	SignIn: "/sign-in",
	Error: "/error",
	signUp: "signup" // new signup page, but with missing "/"
}
```

If you don't have a test for routing to the sign-up page, you might overlook bugs in incorrect routing. Additionally, if you have conventions or rules in place, it's essential to reflect these in your types.

To address the first issue, you can create a type for the PageRoute object using template literal types. This enables you to incorporate template literal strings into your type definitions using the same syntax as JavaScript.

```typescript
type PageRoutes = Record<string, `/${string}`>

const PageRoutes = {
	Home: "/",
	SignIn: "/sign-in",
	Error: "/error",
	signUp: "signup" // TS is complaining now - Type '"signup"' is not assignable to type '`/${string}`'
} as const satisfies PageRoutes
```

Since I'm using capitalized names for routes, I can also reflect this in my types using the `Capitalize` utility type in TypeScript.

```typescript
type PageRoutes = Record<Capitalize<string>, `/${string}`>
```

Even though I've fixed the missing "/" in the sign-up page path, I can still see TypeScript correctly complaining:

```typescript
type PageRoutes = Record<Capitalize<string>, `/${string}`>

const PageRoutes = {
	Home: "/",
	SignIn: "/sign-in",
	Error: "/error",
	signUp: "/signup" // TS is complaining now - Object literal may only specify known properties, and 'signUp' does not exist in type 'PageRoutes'.
} as const satisfies PageRoutes
```

Don't hesitate to narrow your types if you have some less obvious rules. With simple template literals, you can safeguard your functionality and conventions.

[Code playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBACgQwOYFMBKICuUURgXhgG8AoASAAkQBbFALhgCIB6RgGnIGUBLJMASTAMWEXmAC03MO3IBRAE7yQ84cxSLlMsqL4BVAA7CdYTPsYxmzGGBQB3GMdMx9yFGxgAjbDFvcoACxhqbghRMCQmVhIAXxISKABPfRR4VwxsXAIYNBRQeQATAB4AYQR9PwQAG24ALxRC6HkpJAA+dwADZgASIkbm6PaWuNBIWAAVJJR8xFR0nAgGGfQseazSShp6SK0ePkFVY0lpDjIFJRVI9XOdsQMjMVNzSxgxzhgQmFBqfUqEKWaYOIXpMYAByRiOMyg954MAgWAIUJiBAeSopKAgGCJZJgzo9PrhAagmLDcDQYHJaZpFa4ABMi2pGTwhHWVFoqhueyEkUOUi0Z2UqiumhOxjuPIeZgsVleMM+NB+fzAAKBAHkPAArXKwao4eRVIIIBIwcCVY0QZLAbgAM2NAGs4bYwM4lMl5FBuLh3AgwPkwWL9ND8iBMnDYCgAB4hWBSLEg0FLOa4YmxEgjckTSmJmkQADMDNmObW5DZWxYnIE3JEYiO-I0FxYwvkFfF1b4jxiQA)

## 2. Help Typescript to know your types

Another common scenario in code is having arrays of items where some of the items can be nullable or undefined. If I want to retrieve only non-falsy values, I can use the `Array.filter` function:

```typescript
const items = [{name: "Sudolabs"}, null, undefined, {name: "Typescript"}]

const filteredItems = items.filter(Boolean).map(item => item.name)
```

This works perfectly on the code level. All falsy values are filtered out. However, for some reason, TypeScript is still complaining when hovering over the item in the `Array.map` function.

```typescript
'item' is possibly 'null' or 'undefined'.
```

In this situation, TypeScript needs assistance in knowing that the filtered values are no longer present in the array. This is where a [Type Predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) comes into play. A Type Predicate should be used as the return type of a function in which we check if some value matches the interface or not. In this case, we need to include a type predicate in our Boolean function to inform TypeScript that the value is not null or undefined. It will look like this:

```typescript
type Item = {
  name: string
}

const filterItems = items.filter((item): item is Item => Boolean(item)).map(item => item.name)
```

We can even move it to a separate function:

```typescript
const items = [{name: "Sudolabs"}, null, undefined, {name: "Typescript"}]

type Item = {
  name: string
}

const isItemDefined = (item: Item | null | undefined): item is Item => Boolean(item)


const filterItems = items.filter(isItemDefined).map(item => item.name)
```

By introducing generics, we can make this function universally applicable for filtering out falsy values:

```typescript
const isDefined = <T,>(item: T | null | undefined): item is T => !!item

const items = [{name: "Sudolabs"}, null, undefined, {name: "Typescript"}]

const filterItems = items.filter(isDefined).map(item => item.name)
```

[Code playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBAllApgWwjAvDA2gbzAQ2UQC4YAiAZQFcATEAG3wCMIyBfAGhjCvvq6pgaiAGZwwiGlzyES5ACoBPAA6IIwAE5xlUdgF0AUAdCRYY+kg2SAkklQZ4diADpzlgBQAhEA0T4wAJTOyPjK7ggoGAB8jijOBEQBRgD0yTAUDFRQcOAGUCqIMLaRmDgGMNyypNBaYADmBmxGJtAw+ao0AGJwFohWNMX2mBGorj0e4XYBpCPwaIPRMN6+-pMoAUEhYbPoMSPxskkGqemZ2eAwAO4IABYwAOKIElrAEMbgrXAQACKi4pIOAA88g4UTWyFI8hgAB9uLx6DCYIJhGIJDRprFkHMYFDdjAAIT4kbND6wdqSbq9fqDCAAJgcIxcbj64R+fzRm1C4MW+wSiACQA)

## 3. Make the Typescript precise

Imagine a scenario where you have two types of users in your system: one for suppliers and one for customers.

```typescript
type Customer = {
    id: string,
    role: "customer"
    firstName: string,
    lastName: string
}

type Supplier = { 
    id: string,
    role: "supplier",
    businessName: string
}

type User = Customer | Supplier
```

If you want to retrieve only users with specific roles, you can create the function **getUsersByRole** like this:

```typescript
const users: User[] = [{id: "1", role: "customer", firstName: "Oliver", lastName: "Dendis"}, {id: "2", role: "customer", firstName: "Foo", lastName: "Bar"}, {id: "3", role: "supplier",businessName: "Sudolabs"}]

const getUsersByRole = (role: 'customer' | "role") => {
   return users.filter(user => user.role === role)
}

const customers = getUsersByRole('customer') // customers is type Users[]
```

It works perfectly! Your function now returns only users with the role "customer". Now, you can be confident that all users have a first name and last name. However, TypeScript will not allow this.

```javascript
const customerNames = customers.map(customer => customer.firstName) // Property 'firstName' does not exist on type 'User'. Property 'firstName' does not exist on type 'Supplier'.
```

The thing is that Typescript inferred the type of customers as `User[]` , even though we know it should be `Customer[]`. In this situation, we know more than Typescript and that’s awesome because it means that there are still some fields where humans can beat the machines!

In this situation, we need to make the Typescript precise. To address this, let's introduce some utility types. We need a utility that returns the correct type (`Customer` or `Supplier`) based on the provided role. It can be implemented like this:

```typescript
type GetUser<T extends User["role"]> = Extract<User, { role: T }>

type Foo = GetUser<'customer'> // customer type
type Bar = GetUser<'supplier'> // supplier type
```

With such a utility, we can create a better-typed version of `getUsersByRole.` By specifying the return type of the function based on its role using the `GetUser` utility type, we can enhance our `getUsersByRole` function.

```typescript
const getUsersByRole = <T extends User['role']>(role: T) => {
	return users.filter((user): user is Extract<User, { role: T }> => user.role === role)
}
```

In this case, TypeScript infers types correctly, and there are no longer any complaints:

```typescript
const customers = getUsersByRole('customer') // customers is type Customer[]

const customerNames = customers.map(customer => customer.firstName) // TS is happy!
```

[Code playground](https://www.typescriptlang.org/play?ts=5.4.2#code/C4TwDgpgBAwgrgZ2AewLYQE5QLxQN4BQUxUAlgCYBcUSGpAdgOYA0RJGyANhNQEQDGiFOgy82xAGakMSAHIBDdNVoMW4qJ3lzFPGsDpMCAXwIFQkKAGU4YMJ1KYc+KOorL9q1iSgdufBDZ2DqJeJABGiAwQCAgKSnoGjMam5tAAqgiOuPBIaI4APlaB9pim-Mj0SFCImAjUGZgA2gC6To14blC8AIy8zD5cugJCeSFQUjLAcUMA8vYAbph9GlpTOnwAIhD05KQIvEb9HVRdAEzLvkOCuSLLE9rxvABiyMjLmg9DAELyoof4nV4AGYLoN-MVgn0IggojFpnxrOQuPIwvsjM0yhUqowIMAGjIviAAEqDJwACku1AA5NdhJgqVBCrxLrwAJQ4AB8+HEGFxcAw9GqmRkADopJxgJgyTUsNguTKRZccNhcJdWckCOVKsAoLTRggnDi8cKEISSdwyTSRiIqeyAPR23XW2pkA2pKD4hAtTHap03TDTA24PUiBAi1DyMBkkNZLkxjBi6Sfe2OgAKHEgGFAUCp9zW6AZSOiUHoyB1EAAHnsdRUoO6qfiqSKoOnkJns7mk-mIIXkMXS+Wq1Va-XrLYShgm6YHVYuHBgKQKmZwNAAOK4-EAHgAKlBK5Kdgb8Y1mYNeM0ubgAKIV-TyfjATf4o4DPxQXdGDk+7G47cr8iemapK4Due63ts5BHsKJ4sheFJgu+rKUOuxqYJuqTIBIr4QByLScty7B8gKQq1ImEpSjK+EKkqKqqoM7JaFAKFbhhWGXLhGImJqWI6qk5A5HSMiGr+-6AcSgyWvGtpQDO8YGnsdYrrAzoYN63G+nxAmjIGTiaSpYYRlG8b4fGiaTNMKbvpYrpQAAFpGYAgAAhEAA)

## Wrapping up

Here, we explored some lesser-known features of TypeScript that significantly enhance code quality and type safety. The art of TypeScript lies in writing types as narrow as possible yet general enough to be reusable across multiple use cases. Through three illustrative examples, we showcased how minor adjustments in code, such as TypeScript utility types and type predicates, can help TypeScript robustly safeguard our functionality. We will continue in the next article!
