Improving VS Code's Intersected Type Hints

21st Mar 2023

source

Recently at work I’ve been finding myself dealing with a lot of intersected types that are combinations of other complex types. Usually this is fine because I’ll be familiar with the types I’m working with, but there are instances where I’ll need to look at their definitions, and it’s at this point I realise that the intersected type hinting in VS Code is a little lacking. All too often I find myself knee deep in node modules or in library codebases looking at type definitions to understand the shapes of objects I’m working with.

In this post, I’ll be referring to these types I’ve created for illustrative purposes:

type ResponseT = {
  code: number;
  message: string;
};

type Item = {
  id: number;
  name: string;
  description: string;
};

type Data = {
  items: Item[];
  numOfOrders: number;
};

type FetchData = { isLoading: boolean } & ResponseT & Data;

If I were to hover over FetchData, you’ll see the problem I’m describing. Unless you have knowledge of ResponseT and Data, it’s not a very informative type hint.

FetchDataHint

I initially looked for some VS Code settings to change but stumbled across a couple of GitHub discussions that described similar complaints. Yet, no proposed fix or feature was found to be implemented any time soon.

Armed with my latest findings, I looked to other solutions for my problems and decided to see if I could find a type alias that would resolve these intersected types to aid hinting. I had my reservations about this path though. It doesn’t feel great using code to solve what is essentially a tooling problem, but as there were no other alternatives I decided that there were better things to worry about!

My first try at solving this was to create a type alias, which we’ll call Expose. Here we’re passing through a generic T, which uses conditional type inference to copy it into O, and then we use a mapped type which goes through the copied type’s properties. This way, we’ll have all our properties resolved into a base object type.

type Expose<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

Success! Here we can see the type hint now resolves one level deep, and we’re able to see all the fields that were previously hidden in ResponseT and Data.

ExposedDataHint

Or so I thought… I soon found that if I tried to use this on a function interface, the resolved type would be an empty object. I was running into TypeScript errors left and right telling me that functions were not callable because of this.

Taking this definition as an example, we’ll see that it doesn’t quite work how we want it to:

interface SomeFunction {
  (...args: string[]): FetchData & { error: boolean };
}

ExposedFunction

Back to the drawing board! I did some searching around and stumbled across a StackOverflow post that described a very similar problem to what I was having, with some interesting answers.

I found this alias, which followed a similar pattern as above, except going further to implement support for functions too:

export type Expose<T> = T extends (...args: infer A) => infer R
  ? (...args: Expose<A>) => Expose<R>
  : T extends infer O
  ? { [K in keyof O]: O[K] }
  : never;

The first half uses more conditional type inference to check if it’s a function, and if so, resolves the arguments and return type. If it’s not a function, it’ll continue as above to perform the same process of resolving the type properties.

So now we have a robust type alias that improves our type hinting including functions!

ExposedFunctionHint

But why stop there? What if I wanted to know what the Item[] type was too? Thankfully its quite simple: all we have to do is change the mapped type to recursively call our type alias and pass the key through, like so:

export type ExposeRecursively<T> = T extends (...args: infer A) => infer R
  ? (...args: ExposeRecursively<A>) => ExposeRecursively<R>
  : T extends object
  ? T extends infer O
    ? { [K in keyof O]: ExposeRecursively<O[K]> }
    : never
  : T;

With this, we can now go even further than one level deep to see the resolved types of everything:

RecursiveHint

So there we have it: two TypeScript type aliases to improve VS Code’s intersected type hinting. These can now be easily used in library to ensure that people using them can easily understand the expected shapes of objects and more!

It’s worth noting that the type hints can get quite lengthy, so I’d recommend to set noErrorTruncation to true in your tsconfig.json to ensure you’re not losing out on any information.