Creating auto-complete types without restricting values in TypeScript
Tags: typescript, programming, frontend, webdev
Published: 2022-09-14 21:07:37

---
title: Creating auto-complete types without restricting values in TypeScript
published: true
date: 2022-09-14 00:00:00 UTC
tags: TypeScript, programming, frontend, webdev
canonical_url: https://eddiehinkle.com/articles/creating-autocomplete-types-without-restricting-values-in-typescript
---

## The problem

In my current role, we build internal libraries for use by our front-end developers. For a new library we're building we needed to do something I hadn't ever done before. Our library is in TypeScript and we wanted to provide IDE suggestions for the values of a parameter, but we couldn't actually restrict the value of that parameter. This is because 90% of the time, the value would be a couple of obvious values, however, the other 10% of the time, we had no way to predict what the value would be!

## The challenge

You would think that the solution would be simple. But it wasn't.

If you are familiar with TypeScript the initial problem seems easy. We need to declare a list of insects that you can use. This is easily accomplished by defining a Union Type.

```
type Insect = 'scorpion' | 'cockroach' | 'caterpillar';
```

The thing that's missing is that if I find a new insect and try to use it, I can't:

```
const foundInsect: Insect = 'bee';
```

That would cause an error.

Well, there is a type that would accept any of these strings, right?

```
const foundInsect: string = 'bee';
```

This would work, but we lose our IDE suggestions because now the IDE thinks we should just add _any_ string value.

The next assumption is if we have our Insect type as a union, why don't we just include the string type? That's one of the first things I thought to try!

```
type Insect = 'scorpion' | 'cockroach' | 'caterpillar' | string;

const foundInsect: Insect = 'bee';
```

At first look, this seems like it should work. We define our list of approved values and we also allow it to be a string.

This won't throw an error, but it turns out ... we still lost our IDE suggestions!

It turns out that

```
type Insect = 'scorpion' | 'cockroach' | 'caterpillar' | string;
```

gets simplified down to

```
type Insect = string;
```

which means no IDE suggestions for us!

That is when I discovered [this issue on the TypeScript GitHub repo](https://github.com/microsoft/TypeScript/issues/29729), where this problem has been discussed at length.

The problem is, we need to say that the final segment, "string", includes all strings _except_ the ones we've defined. By doing that, TypeScript understands that it needs to hold on to the original literal string types that we define upfront.

The solution, provided by [@spcfran](https://github.com/spcfran) and [@manuth](https://github.com/manuth) is:

```
type LiteralUnion<T extends string | number> = T | Omit<T, T>;
```

Which works wonders!

## The solution

All together you end up with:

```
type LiteralUnion<T extends string | number> = T | Omit<T, T>;

type Insect = 'scorpion' | 'cockroach' | 'caterpillar';

const foundInsect: LiteralUnion<Insect> = 'bee';
```

The first type, LiteralUnion is what's called a Generic. It needs a second type to be fully defined. In that definition, the letter T represents whatever type you provided.

So LiteralUnion takes another type called "T" which can extend either a string or a number. Then LiteralUnion defines itself as either: the type, or everything that doesn't match the type.

That's a bit abstract, so let's dive a little deeper.

The second type, Insect is just what we've been working with. It's a Union type, so it says that a value can be "A or B or C". In this case, we're saying the value can be `scorpion` OR `cockroach` OR `caterpillar`.

When we combine these two types: `LiteralUnion<Insect>` we're saying that the generic type for LiteralUnion is going to be Insect. so `T = 'scorpion' | 'cockroach' | 'caterpillar'`. This means that ultimately when the LiteralUnion's type is: `T` OR `OMIT T`, we essentially get:`type LiteralUnion = 'scorpion' | 'cockroach' | 'caterpillar' | OMIT<string, 'scorpion' | 'cockroach' | 'caterpillar'>`, which is exactly what we need in order for TypeScript to hold on to all the types for them to be used by the IDE to suggest them as options!