Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tooltips / IntelliSense: Don't resolve type aliases (aka "semantic sugar") set explicitly #31940

Open
Sgt-Nukem opened this issue Jun 17, 2019 · 29 comments
Labels
Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Milestone

Comments

@Sgt-Nukem
Copy link

We have defined a bunch of "semantic sugar" type aliases like:
image

Status quo (bad): VS Code is resolving these along the alias chain in tooltips and IntelliSense suggestions (which kinda makes non-sense of the type alias in the first place)
image(c.f. hovered tooltip over this.url at bottom)

Better: Let VS Code show UrlString as type here instead of string to aid developers with semantic sugar as of what type of string to expect.

Alternative: Show both, i.e. BufferingWebSocket.url: UrlString (= string).

@mjbvz mjbvz transferred this issue from microsoft/vscode Jun 17, 2019
@mjbvz mjbvz removed their assignment Jun 17, 2019
@MartinJohns
Copy link
Contributor

From the documentation: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases

Aliasing doesn’t actually create a new type - it creates a new name to refer to that type.
Type aliases don’t create a new name — for instance, error messages won’t use the alias name.

So it seems to be intentional, tho I personally would hope for a change regarding this as well.

@Sgt-Nukem
Copy link
Author

It is NOT a new type of course, it's just an alias - I mean: That's the whole point of an alias.

Nonetheless nothing contradicts showing the alias in tooltips (or both - c.f. Alternative in my OP) IMHO.

Aliasing doesn’t actually create a new type - it creates a new name to refer to that type.
Type aliases don’t create a new name — for instance, error messages won’t use the alias name.

These two sentences are contradicting another, aren't they?
--> it doesn't CREATE TYPE - it CREATES NAME
--> it doesn't CREATE NAME ???

@OliverJAsh
Copy link
Contributor

Given:

// ./provider.ts
export type DOMFile = File;
// ./consumer.ts
import { DOMFile } from './provider';

type File = {
  id: string;
  file: DOMFile;
};

Hover over File and you'll see:

type File = {
  id: string;
  file: File;
};

The type inspection shows file: File when I expect it to show file: DOMFile. This makes it look like a recursive type, but actually this File type is a different type entirely. This creates a very confusing user experience.

This issue does not apply if we use interfaces:

// ./provider.ts
export interface DOMFile extends File {}

Is there a reason for type aliases to have different behaviour to interfaces in this context?

@martaver
Copy link

martaver commented Feb 8, 2020

I have also found, repeatedly, that it would be far more useful to simply show the developer the name of the type being referenced in intellisense, rather than trying to resolve the type definition.

Actually, it would be useful. Fullstop. Since descriptions in intellisense like this, really aren't:

(method) Observable<{ [P in keyof SourceMap<TSourceSpec>]: SourceMap<TSourceSpec>[P] extends SourceInput<infer U> ? U : never; }>.subscribe(next?: (value: { [P in keyof SourceMap<TSourceSpec>]: SourceMap<TSourceSpec>[P] extends SourceInput<infer U> ? U : never; }) => void, error?: (error: any) => void, complete?: () => void): Subscription (+4 overloads)

If it kept the type name, I could actually go to the type, read its documentation etc. and better understand the intent.

Understand that type aliases are 'just aliases'... but this is really just about creating a better developer experience.

Personally, I never really understood why there needed to be separate 'type' and 'interface' entities.

@Sgt-Nukem
Copy link
Author

Actually, it would be useful. Fullstop. Since descriptions in intellisense like this, really aren't:
´´´
(method) Observable<{ [P in keyof SourceMap]: SourceMap[P] extends SourceInput ? U : never; }>.subscribe(next?: (value: { [P in keyof SourceMap]: SourceMap[P] extends SourceInput ? U : never; }) => void, error?: (error: any) => void, complete?: () => void): Subscription (+4 overloads)
´´´

OMG!! I've never seen the correlation to my feature request until now!!

Yes, I fully second this. Error messages would be far more readable if Shape stayed Shape and wouldn't be blown up to 'Square' | 'Rectangle' | 'Circle' | 'Ellipse' | 'Triangle' or something...

@Polatrite
Copy link

Polatrite commented Apr 16, 2020

+1 for this.

In our project, I wanted to declare some semantic types representing numbers, to remove some of the ambiguity of "what does this random float value actually mean?", so I added these type alias declarations:

type percentage = number
type timeInSeconds = number
type timeInMilliseconds = number
type radians = number
type degrees = number
type gameUnits = number

I was quite disappointed to see that the assorted tooltips/intellisense did not keep the alias in tact, and just resolved down to a number.

@marlemie
Copy link

marlemie commented Jun 8, 2020

I just stumbled across this behavior as well and would like to see the recommended change. Having type aliases can make code clearer and easier to read. But when this information gets erased in the tooltip, it looses its purpose.

@svdHero
Copy link

svdHero commented Jul 1, 2020

+1

It gets even worse with generic functions. For rich type modelling, type hints like this:

(methode) toResult<Readonly<{
    searchIndexId: string;
    searchResult: Readonly<{
        pageInfo: Readonly<{
            pageIndex: number;
            totalNumPages: number;
            numItemsOnThisPage: number;
            numItemsPerPage: number;
            totalNumItems: number;
        }>;
        pageData: readonly Readonly<...>[];
    }>;
}>>(response: ServerResponse<...>): Result<...>

are almost useless. I can't even see the function parameter's full type, because all the space is used up for the generic type description.
What the above should have said, is:

(methode) toResult<SearchForProjects>(response: ServerResponse<SearchForProjects>): Result<string, SearchForProjects>

honoring my type aliases.

@carljohnson93
Copy link

Faced same issue when wanted to short Record<string, unknown> to Dict via type Dict = Record<string, unknown>, as there was alot of them, and saw this alias nowhere! For example following function signature:

function foo<
  T1 extends Dict,
  T2 extends Dict,
  T3 extends Dict,
  T4 extends Dict
>(
  param1: T1,
  param2: T2,
  param3: T3
): T4

Showed in tooltip as:

function foo<T1 extends Record<string, unknown>, T2 extends Record<string, unknown>, T3 extends Record<string, unknown>, T4 extends Record<string, unknown>>(param1: T1, param2: T2, param3: T3):  T4

I was very frustrated when faced this problem! Please fix this or at least add setting to change this behavior! Definitions are just unreadable!

@tiansivive
Copy link

tiansivive commented Dec 29, 2020

+100 for this!

Just wondering if there's been any progress?

In my mind there's no obvious drawback for this. When working with complex types, it quickly makes the tooltip obscure and hard to understand the error. At the very least, an option to show alias instead of resolving should be enough.
What prompted me to write this comment was the need I had to temporarily replace the aliased type with a simpler version just so I could understand the tooltip.
I spent an absurd amount of time trying to understand the type resolution and not understanding why the piece of functionality broke when all it was trying to tell me was that I'd forgot to type a particular field as optional. This was completely inline with the changes I was doing but the unhelpful tooltip sent me down a rabbit hole. As soon as I changed the aliased type to something simpler I was able to pick up the problem immediately.

Just to extra emphasise this, consider the following, in React:

type MyElem = ReturnType<React.FC<SomeProps>>
type ElemSet = MyElem | OtherElem | AnotherOne

This quickly gets incomprehensible if MyElem is resolved, even more so if the other elem types are also resolved.

@b6i6o6
Copy link

b6i6o6 commented Jan 18, 2021

I second this suggestion as well. If I create an alias, it's specifically to see the alias rather than seeing the true type which is often more complex. An option to resolve the alias on demand inside the tooltip would be nice but not necessary.

@martaver
Copy link

martaver commented Jan 18, 2021 via email

@yuhr
Copy link

yuhr commented Feb 12, 2021

I hugely appreciate this feature (or option). Showing internal structure instead of type alias name has never made any sense to me when I want to see the type of an identifier in a tooltip. This suggestion definitely helps the "Documentation as Code" idea.

@yuhr
Copy link

yuhr commented Mar 4, 2021

This issue is probably related to #202. Once we get true nominal typing in TS, then IntelliSense tooltips will be no longer allowed to replace such types entirely to their structures (though it may be good to show structures as annotation like the "Alternative" proposed in the original post).

@Dervisevic
Copy link

I honestly thought i had made a mistake when the alias type did not show up in intellisense. It really doesn't make sense to me when you set the alias explicitly like this.

@plaa
Copy link

plaa commented Apr 13, 2021

I just started using yup for input validation. An awesome aspect of it is that it produces the types out of the validation schema, so they're guaranteed to match. But Intellisense and error messages becomes completely useless when you get this:

Example playground

intellisense-sucks

@iPherian
Copy link

iPherian commented Apr 13, 2021

Like the above, I sometimes also like to see the type with aliases resolved.

But not all the time. Perhaps it could be when you hold down a key before mousing over something, it resolves all the aliases, otherwise it keeps them. Like shift maybe.

@miyauchiakira
Copy link

I found an ugly workaround by using Union Type.

ss

In this case, sadly, numerical type must be type casted to number when you use in formula. String type are not.
I've came up with this workaround now, so there might be other hidden side effects.

Using it for qualitative variable must be useful and less side effects. like

type SomeId = number | Number

@plaa
Copy link

plaa commented May 12, 2021

@miyauchiakira A better workaround is branded types / nominal types. In this case you need to cast the original value (when you know it's of a certain type) but you can use a variable like the original type:

type Degrees = number & { _brand: 'Degrees' };
type Radians = number & { _brand: 'Radians' };

const deg: Degrees = 90 as Degrees;
const rad: Radians = (deg / 180 * Math.PI) as Radians;

const bad: Radian = deg;  // ERROR

Playground

It's strange that some types are expanded, but for example Degrees in the above case is not. 🤔

My issue was that auto-generated types which can be very complex cause the error messages to become illegible, instead of displaying the (explicitly set) alias type.

@miyauchiakira
Copy link

@plaa
Thank you. That's an ideal solution!
What about using "nominal types" also in your case?
Tooltip shows the alias name.

export declare type Product = yup.InferType<typeof productSchema> & { _brand: 'Product' };

Playground

@yuhr
Copy link

yuhr commented May 12, 2021

@plaa @miyauchiakira By the way, strictly speaking, the proper term for that technique is "branded types" (or "type branding"). It is a workaround to mimic nominal typing in structural type system, but not "nominal types" itself.

@plaa
Copy link

plaa commented May 15, 2021

@miyauchiakira True, just adding & {} (which does not change the type at all) causes TypeScript not to expand the type. I have no idea why this workaround works. Ugly, but effective – still would prefer TypeScript to "just work".

@user72356
Copy link

user72356 commented Oct 8, 2022

@miyauchiakira True, just adding & {} (which does not change the type at all) causes TypeScript not to expand the type. I have no idea why this workaround works. Ugly, but effective – still would prefer TypeScript to "just work".

This suggestion doesn't work with:

type TaskInfo = Database["public"]["Tables"]["tasks"]["Row"] & {};

However, this works:

type TaskInfo = Database["public"]["Tables"]["tasks"]["Row"] & { _: ""};

I have no idea why.

@ptrxyz
Copy link

ptrxyz commented Jan 14, 2023

+1 for this.

Maybe this would work: by default, hovering over a type alias should simply show it's name, however while also holding CTRL key (or anything), it would expand the alias to what it actually is...?

@ghost
Copy link

ghost commented May 20, 2023

type alias<t> = t & { _:never }
type my_type = alias<number | { "any": "complicate type" }>

worked for me

@Hetch3t
Copy link

Hetch3t commented Sep 17, 2023

So issue is 4 years old, however still no solution from the TS team...

Anyways, yet another TypeScript hack to have it working as expected. Since & {} doesn't always work, e.g.:

type HexColorCode = string & {} // Works fine
type UUID = `${string}-${string}-${string}-${string}` & {} // Will show `${string}-${string}-${string}-${string}` instead of UUID
type DateTime = Date & {} // Will show Date instead of DateTime

, what I generally do is include some method from the original type in {}:

type HexColorCode = string & {}
type UUID = `${string}-${string}-${string}-${string}` & { toString: string['toString'] }
type DateTime = Date & { toString: Date['toString'] };

This fixes IntelliSense problem and doesn't clutter up the code with unnecessary stuff (like { _: never }).

@yemreak
Copy link

yemreak commented Oct 26, 2023

type alias<t> = t & { _?: never } // instead { _: never}
type my_type = alias<number | { "any": "complicate type" }>

Worked for me

If you don't see images click here | Visual explanation of how its work

export type PairId = Readonly<`${AssetSymbol}_${AssetSymbol}`>

Untitled.png

type alias<t> = t & { _?: never }
export type PairId = alias<Readonly<`${AssetSymbol}_${AssetSymbol}`>>

Untitled.png

@dsogari
Copy link

dsogari commented Feb 22, 2024

For those who want the opposite behavior (i.e., resolve the alias), use this:

type Alias<T> = T extends T ? T : T;
type Resolve<T> = T & unknown;

type Degrees = Alias<number | undefined>; // this works
type Radians = Resolve<Alias<number | undefined>>; // this works
type ResolveDegrees = Resolve<Degrees>; // this doesn't work

const deg1: Degrees = 0;
const deg2: Degrees = undefined;
const deg3: Degrees = {}; // error

const rad1: Radians = 0;
const rad2: Radians = undefined;
const rad3: Radians = {}; // error

@cbn-falias
Copy link

cbn-falias commented Mar 22, 2024

Had similar issues with Zod and z.infer.
I noticed that a Omit<MyType, ''> would already do the trick, so I destructured the utility type a bit and came up with this very simple type:

type Alias<T> = Pick<T, keyof T>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests