How TypeScript template literal types helped us with multiple database support
Tags: typescript, database, sqlserver, postgres
Published: 2021-05-18 08:30:37

---
title: How TypeScript template literal types helped us with multiple database support
published: true
date: 2021-05-11 11:56:52 UTC
tags: Typescript,Database,SQLServer,Postgres
canonical_url: https://hasura.io/blog/how-typescript-template-literal-types-helped-us-with-multiple-database-support/
---

![How TypeScript template literal types helped us with multiple database support](https://hasura.io/blog/content/images/2021/05/typescript_multiple_dbs_v2.png)

Last year November's release of TypeScript was one of the most exciting releases. Literal types took over the TypeScript community by opening a whole new set of possibilities. The community created many amazing projects: router parameter parsing, JSON parser, GraphQL typed AST, SQL query validation, CSS parsing, games, [a database implemented purely with TypeScript types](https://github.com/codemix/ts-sql), and many more. At Hasura, we recently had an interesting use case as well. But firstly — what are the literal template types?

## A few words on template literal types

We already had [<u>Template literals</u>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) in JavaScript that provide a convenient way to have values inside of strings. For example:

```
const variant = `button_${size}_${color}`

```

In TypeScript, the type of the above value would be a string, which is fair, but can we have more? Imagine that we know exactly those values because there are only two possible sizes and three possible colours. We could do this:

```
type Size = "small" | "big"
type Color = "green" | "red" | "white"

type Variant = 
  | "button_small_green" 
  | "button_small_red" 
  | "button_small_white" 
  | "button_big_green"
  | "button_big_red"
  | "button_big_white"

```

Better, but... maintaining the Variant type seems like a nightmare, doesn't it? Every time we extend either `Color` or `Size`, we need to add new literals to our `Variant` union. That's when TypeScript 4.1 comes to the rescue. We can use template syntax not only for values, but also for types! Thanks to template literal types we can simplify the above code:

```
type Size = "small" | "big"
type Color = "green" | "red" | "white"

type Variant = `button_${Size}_${Color}`
```

Isn’t it great? Now, let's see why we needed template types and what's our use case.

## Multiple database support in the Hasura Console

In the Hasura 2.0 release, we introduced support for multiple databases. You can not only connect many databases to your Hasura, but also you can connect different databases! Postgres, BigQuery, MS SQL Server, and soon more. They now can all be managed via Hasura Console. However, each of them supports a different set of features. For example, some DDL functionalities are disabled for Big Query. MS SQL Server handles fetching data differently. A few features are not yet supported for a particular database. There are a lot of cases to handle!   
We could do this:

```
if (currentDatabase === "postgres") {
   // render a feature
else {
  // don't
}
```

or

```
const supportedDatabases = ["bigquery", "postgres"]

const Component = ({ currentDatabase }) => {
  if (!supportedDatabases.includes(currentDatabase) {
     return null
  }
  // ...
}
```

But I think we can agree that it won't scale well. Especially if we need to handle those cases in dozens of components. And when we add a new database, we'd need to go over it again.

We needed some abstraction for that. Basically we needed something like this:

```
if (isFeatureSupported('tables.browse.enabled')) {
  return (
    <BrowseData {...props} />
  );
}
```

and

```
const supportedDrivers = getSupportedDrivers('actions.relationships');
```

- One source of truth to declare enabled features for each database that can be extended in any way and multiple levels deep.
- A function that, for a given feature, will tell us if it's supported based on the currently active database.
- A function that, for a given feature, will tell us all the supported databases (needed when filtering out unsupported databases).

Our source of truth can be extended in any way. It can be multiple levels deep. It can be nested as well.  
Here's an example of a database’s features configuration:

```
export const supportedFeatures = {
  driver: {
    name: 'mssql',
  },
  tables: {
    create: {
      enabled: false,
    },
    browse: {
      enabled: true,
      customPagination: true,
      aggregation: false,
    },
    ...
  },
  events: {
    triggers: {
      enabled: true,
      create: true,
      edit: false,
    },
  },
  ...
};
```

While the `isFeatureSupported` and `getSupportedDrivers` functions are fairly straightforward to implement, it's the developer experience that we needed to be enhanced. We didn’t want to type `tables.browse.enabled` by hand. We didn’t want to check the supported features object every time to see the shape. We wanted to have it autocompleted!  
After all, they don't make those memes without reason.

![How TypeScript template literal types helped us with multiple database support](https://paper-attachments.dropbox.com/s_5877C0592376509AD271DE9E9625D08788201756CC6938E001E2B6F1FEB9EB25_1619725723645_Screenshot+2021-04-29+at+21.48.36.png)

##   
Taking advantage of template literal types

We used template literal types to have a type-safe string dot notation. The following types `Path` and `PathValue` allowed us to have a function `get` that accepts an object and a path as string dot notation and returns a values under the path.

```
type Path<T, Key extends keyof T = keyof T> =
  (Key extends string
  ? T[Key] extends Record<string, unknown>
    ? | `${Key}.${Path<T[Key], Exclude<keyof T[Key], keyof Array<unknown>>> & string}`
      | `${Key}.${Exclude<keyof T[Key], keyof Array<unknown>> & string}`
      | Key
    : never
  : never)

type PathValue<T, P extends Path<T>> =
  P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
    ? Rest extends Path<T[Key]>
      ? PathValue<T[Key], Rest>
      : never
    : never
  : P extends keyof T
    ? T[P]
    : never;

declare function get<T, P extends Path<T>>(obj: T, path: P): PathValue<T, P>;
```

I won’t go into the details of the above code. There are already plenty of amazing articles explaining how template types work. I link to a few of them at the bottom of this blogpost. I encourage you to check them out!

You can also play with the above code here:  [https://tsplay.dev/mL9dbW](https://tsplay.dev/mL9dbW)

With the `Path` and `PathValue` types and `get` function we were able to implement our database utilities as:

```
export const isFeatureSupported = (feature: Path<typeof supportedFeatures>) => {
  return get(dataSource.supportedFeatures, feature);
};
```

and

```
export const getSupportedDatabases = (
  feature: Path<SupportedFeaturesType>
) => {
  return databasesConfig
    .filter(config => get(config, feature))    
    .map(d => d.driver.name);
};
```

As the result we we have: a) type-safe dot string notation, b) autocomplete 🎉

![How TypeScript template literal types helped us with multiple database support](https://paper-attachments.dropbox.com/s_5877C0592376509AD271DE9E9625D08788201756CC6938E001E2B6F1FEB9EB25_1619726387959_Screenshot+2021-04-29+at+21.59.42.png)

##   
Summary

About a year ago, we decided to adopt TypeScript, which definitely empowered us to write code with confidence. It not only allows us to write safer code but also hugely improves our developer experience. Template literal types were a big help when implementing multiple databases support, and we're excited to explore more use cases for them!

Do you have any use cases for template literal types that you'd like to share? Let us know in a comment!

### Further reading:

Learn more about TypeScript template literal types:

- [TypeScript 4.1 announcement post](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/)
- [I need to learn about TypeScript Template Literal Types](https://dev.to/phenomnominal/i-need-to-learn-about-typescript-template-literal-types-51po)
- [TypeScript: Template Literal Types](https://michalzalecki.com/typescript-template-literal-types/)
- [TypeScript Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html)

​​If you are interested in learning more about how we addressed some of the other technical complexities of multiple database support, check out the post [Building a GraphQL to SQL compiler on Postgres, MS SQL, and MySQL](https://hasura.io/blog/building-a-graphql-to-sql-compiler-on-postgres-ms-sql-and-mysql/).