DEV Community

Eddie
Eddie

Posted on • Originally published at eddiehinkle.com on

Creating auto-complete 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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

That would cause an error.

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

const foundInsect: string = 'bee';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

gets simplified down to

type Insect = string;
Enter fullscreen mode Exit fullscreen mode

which means no IDE suggestions for us!

That is when I discovered this issue on the TypeScript GitHub repo, 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 and @manuth is:

type LiteralUnion<T extends string | number> = T | Omit<T, T>;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)