Create autocomplete helper which allows for arbitrary values

Sometimes you run into an issue where you want some autocomplete, but not total autocomplete. For instance, here, we have a React component, which takes in a prop, which is a size. And this size here can be either sm or xs.

export const Icon = (props: IconProps) => {
  return <></>
}

const Comp1 = () => {
  return (
    <>
      <Icon size="xs"></Icon>
      <Icon size="something"></Icon>
    </>
  )
}

But imagine that we design an API where you can either pass in sm, xs, or any arbitrary value. What you might think is the right thing to do is to add string to the IconSize union type.

type IconSize = "sm" | "xs" | string 

This seems to make sense, except that you lose all the autocomplete on the size prop, which isn't really what you want. The reason this happens is that TypeScript is quite clever with union types, and it tries to kind of pull in all of the types that it can.

For instance, if you specify the same type twice, like xs, then it won't show up twice in the autocomplete.

type IconSize = "sm" | "xs" | "xs" 

The reason this is happening with a string, is that TypeScript is reasoning since xs and sm are both strings, and you're also specifying string, that it means it should just removed xs and sm and only offer string. Preventing your from getting autocomplete.

The way we can get around this is we can actually use the Omit utility type, and we can omit xs or sm from the union. And now TypeScript won't collapse. these three things, and you'll have autocomplete again.

type IconSize = "sm" | "xs" | Omit<string, "xs", "sm"> 

We can even turn this into a type helper if we want to.

type LooseAutocomplete<T extends string> = T | Omit<string, T> 

This is a way of just taking this information up in our IconSize type and turning it into a type helper. And now we can just wrap sm and xs in this type helper.

type IconSize = LooseAutocomplete<"sm" | "xs"> 

And it's gonna work for us. So again, xs, sm, and any arbitrary value.

Transcript

0:00 Sometimes, you run into an issue where you want some autocomplete, but not total autocomplete. For instance, here, we have a React component which takes in a prop which is a size. This size here can be either SM or XS.

0:16 Imagine that we design an API where you can either pass in SM, XS, or can you pass in any arbitrary value. Here, what you might think is the right thing to do is go "or string" here. This is a union type, so it has SM or XS or string. This seems to make sense, except that you lose all the autocomplete on the size prop, which isn't really what you want.

0:41 The reason this happens is that TypeScript is quite clever with union types. It tries to pull in all of the types that it can. For instance, if you specify the same type twice, like XS, then it won't show up twice in the autocomplete. You'll just have SM and XS.

1:00 The reason that this is happening with the string is it's saying, "OK, SM is a string, and XS is also a string. What we need to do is remove these two, and then only offer string," which is why you don't get any autocomplete. The way we can get around this is we can actually use the omit utility type.

1:19 We can omit XS or SM from the union. Now, TypeScript won't collapse these three things, and you'll have the autocomplete here. You'll have SM, XS, and you'll be able to pass any other arbitrary value. We can even turn this into a type helper, if we want to.

1:37 We can say type loose autocomplete with a T, and we're going to say extend string, just because we're dealing with strings here, and we're going to say T, or we can have omit string T. This is a way of just taking this information up here and turning it into type helper.

2:00 Now, we can just wrap this with loose autocomplete, and it's going to work for us. Again, XS, SM, and any arbitrary value.

Ever wanted just a bit of autocomplete? Here, we create a TypeScript helper called LooseAutocomplete which gives us autocomplete while also allowing arbitrary values.

Picked up this tip from @GavinRayDev - worth a follow!

Discuss on Twitter

More Tips

Play Type Predicates

Type Predicates

1 min

Play TypeScript 5.1 Beta is OUT!

TypeScript 5.1 Beta is OUT!

2 mins

Play How to Name your Types

How to Name your Types

4 mins

Play Don't use return types, unless...

Don't use return types, unless...

4 mins

Play TypeScript 5.0 Beta Deep Dive

TypeScript 5.0 Beta Deep Dive

6 mins

Play Conform a Derived Type Without Losing Its Literal Values

Conform a Derived Type Without Losing Its Literal Values

1 min

Play Avoid unexpected behavior of React’s useState

Avoid unexpected behavior of React’s useState

1 min

Play Understand assignability in TypeScript

Understand assignability in TypeScript

2 mins

Play Compare function overloads and generics

Compare function overloads and generics

1 min

Play Use infer in combination with string literals to manipulate keys of objects

Use infer in combination with string literals to manipulate keys of objects

1 min

Play Access deeper parts of objects and arrays

Access deeper parts of objects and arrays

1 min

Play Ensure that all call sites must be given value

Ensure that all call sites must be given value

1 min

Play Understand how TypeScript infers literal types

Understand how TypeScript infers literal types

1 min

Play Get a TypeScript package ready for release to NPM in under 2 minutes

Get a TypeScript package ready for release to NPM in under 2 minutes

1 min

Play Use assertion functions inside classes

Use assertion functions inside classes

1 min

Play Assign local variables to default generic slots to dry up your code and improve performance

Assign local variables to default generic slots to dry up your code and improve performance

2 mins

Play Know when to use generics

Know when to use generics

2 mins

Play Map over a union type

Map over a union type

1 min

Play Make accessing objects safer by enabling 'noUncheckedIndexedAccess' in tsconfig

Make accessing objects safer by enabling 'noUncheckedIndexedAccess' in tsconfig

1 min

Play Use generics to dynamically specify the number, and type, of arguments to functions

Use generics to dynamically specify the number, and type, of arguments to functions

1 min

Play Use 'declare global' to allow types to cross module boundaries

Use 'declare global' to allow types to cross module boundaries

2 mins

Play Turn a module into a type

Turn a module into a type

2 mins

Play Use deep partials to help with mocking an entity

Use deep partials to help with mocking an entity

1 min

Play Throw detailed error messages for type checks

Throw detailed error messages for type checks

1 min

Play Create a 'key remover' function which can process any generic object

Create a 'key remover' function which can process any generic object

1 min

Play Use generics in React to make dynamic and flexible components

Use generics in React to make dynamic and flexible components

1 min

Play Create your own 'objectKeys' function using generics and the 'keyof' operator

Create your own 'objectKeys' function using generics and the 'keyof' operator

1 min

Play Write your own 'PropsFrom' helper to extract props from any React component

Write your own 'PropsFrom' helper to extract props from any React component

1 min

Play Use 'extends' keyword to narrow the value of a generic

Use 'extends' keyword to narrow the value of a generic

1 min

Play Use function overloads and generics to type a compose function

Use function overloads and generics to type a compose function

2 mins

Play Decode URL search params at the type level with ts-toolbelt

Decode URL search params at the type level with ts-toolbelt

2 mins

Play Use 'in' operator to transform a union to another union

Use 'in' operator to transform a union to another union

2 mins

Play Derive a union type from an object

Derive a union type from an object

2 mins