Why Typescript dynamic tuple types shows all options in Autocompletion/Intellisense?

203 Views Asked by At

I am very new in Typescript and I have an issue which I don't know how to resolve it :(.

Basically I want to create a list of tuple from a list of components. The first element of the tuple is the element's name (keyof MyComponents) and the second elements is its attributes.

(See code below)

playground link

 interface MyComponents  {
    Container: {
      fluid?: boolean
      className?: string
    },
    Tag: {
      text?: string
      className?: string
      hidden: boolean
    }
}

//Get the keys of the list of my components
type Element = keyof MyComponents
 
//Get the attributes depending on the element name
type PickAttributes<T extends Element> = Pick<MyComponents[T], keyof MyComponents[T]>

//Create a mapped tuple type [Element, Attributes]
// and the attributes depend on the element
export type Tuple = { 

  [Element in keyof MyComponents] : [Element, PickAttributes<Element>]

}[keyof MyComponents]

 

const attr : PickAttributes<'Tag'> = {hidden : false} //This works and the auto completion works perfectly

const tuple1 : Tuple = ["Tag", { hidden: false}] //This also works

const tuple2 : Tuple = ["Container", { hidden: false}] //Error but it's normal as the element 'Container' doesn't have the property hidden:boolean

Everything works perfectly but there is a small problem for autocompletion. When I type the first element (Container, Tag, ...), the auto completion of the second element (its attributes) shows all the possible attributes even the wrong ones.

As an example if I type 'Tag' for the first element it suggets me 'fluid' but 'fluid' is only available in 'Container' ! Intellisense shows all options

And when I choose fluid, it also knows it's incompatible...

Typescript knows it's incompatible

So my question is: How can I restrict the autocomplete to only shows valid attributes depending on the element's name ?

Any help will be appreciated ! Thanks !**

1

There are 1 best solutions below

0
On

TypeScript doesn't "know" what the type of the second element in your tuple is based on the first, it only "knows" that they need to "match" in a way. Think of it like this: the way you've written your type Tuple, TypeScript can't figure out whether both elements match each other until you've written both of them. Even if you type "Tag" first, that doesn't give TypeScript enough information to infer what the type of the second element must be. But once you type both elements, TypeScript can at least compare them and validate whether they match. So, no useful intellisense/autocomplete.

We can fix this with generics, but unfortunately they're not terribly straightforward when working with tuples. You have two options:

#1 Make your Tuple type a generic

export type Tuple<T extends keyof MyComponents> = [T, MyComponents[T]]

This does two important things: it sets constraints on what the type of T can be, and it allows TypeScript to infer information about what shape the tuple must have based on T. In essence, you're telling TypeScript: "this tuple will have T in the first element and MyComponents[T] in the second element, so if I pass you "Tag" as T you know exactly what to do!"

The downside is that TypeScript can't infer what T is, you need to explicitly pass it in to the generic type, like so:

const test1 : Tuple<"Tag"> = ["Tag", { hidden: false}];
const test2 : Tuple<"Container"> = ["Container", { h }];

This causes a bit of duplication because not only do you have to pass e.g. "Tag" into the type generic, you have to define it as a literal value on your tuple as well. However, you DO get the enhanced autocomplete you desire; after you have written Tuple<"Tag">, writing the rest of the tuple will intelligently know what possible values there are and limit them accordingly.

#2 (better) Use generic functions

Generic functions are much more powerful and can infer types even if they aren't explicitly defined. Generic type "arguments" and function "arguments" are very similar, so TypeScript can essentially combine them and use the function arguments as a way to directly infer the generic type arguments. Observe:

function makeTuple<T extends keyof MyComponents>(key: T, props: MyComponents[T]) {
  return [key, props];
}

const test1 = makeTuple("Tag", { hidden: true });
const test2 = makeTuple("Container", { fluid: true });

This is pretty nice because not only do you get the same autocomplete/intellisense ability of method #1, but you don't have to duplicate yourself when writing code. It slightly changes the way you would initialize a tuple value, but is still pretty simple and easy to use.

Playground