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

Intellisense documentation lost for remaining properties in expanded Omit<> mapped type #49909

Open
frank-weindel opened this issue Jul 14, 2022 · 5 comments
Labels
Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Milestone

Comments

@frank-weindel
Copy link

frank-weindel commented Jul 14, 2022

Bug Report

I'm attempting to use mapped types over an interface and I want the documentation for the properties from the original interface to be preserved when the mapped interface is implemented by a class. This normally is fine unless the mapper removes some keys from the original interface. The example below actually shows that Omit<> and a 1-level expansion of Omit<> works fine to preserve documentation over the remaining properties. However, if I expand it one more time into a raw mapped type expression, then I suddenly lose documentation.

Unfortunately I can't use either of the first two options in the code I'm trying to write because I'm doing conditional typing on the value side of the map.

Thanks folks!

🔎 Search Terms

omit, exclude, mapped types, documentation, tsdoc, jsdoc, properties, implements, class, hover, intellisense, vscode, quick info

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about mapped types

⏯ Playground Link

Playground link with relevant code

💻 Code

interface MyInterface {
    /**
     * Documentation for prop1
     */
    prop1: number;
    /**
     * Documentation for prop2
     */
    prop2: string;
    /**
     * Documentation for prop3
     */
    prop3: boolean;
}

//
// 1: Omit preserves documentation! (Except for 3.3.3 as Omit wasn't added at that point)
//
type MyModifiedInterface1_DocumentationPreserved = Omit<MyInterface, 'prop3'>;

class MyClass1_DocumentationPreserved implements MyModifiedInterface1_DocumentationPreserved {
    prop1 = 1; // Hover doc: Documentation for prop1
    prop2 = 'a'; // Hover doc: Documentation for prop2
}

//
// 2: So does expanding Omit!
//
type MyModifiedInterface2_DocumentationPreserved = Pick<MyInterface, Exclude<keyof MyInterface, 'prop3'>>;

class MyClass2_DocumentationPreserved implements MyModifiedInterface2_DocumentationPreserved {
    prop1 = 1; // Hover doc: Documentation for prop1
    prop2 = 'a'; // Hover doc: Documentation for prop2
}

//
// 3: But expanding Pick causes documentaiton to be lost!
//
type MyModifiedInterface3_DocumentationLost = {
    [P in Exclude<keyof MyInterface, 'prop3'>]: MyInterface[P];
};

class MyClass3_DocumentationLost implements MyModifiedInterface3_DocumentationLost {
    prop1 = 1; // Hover doc missing!
    prop2 = 'a'; // Hover doc missing!
}

🙁 Actual behavior

In VSCode, hovering over prop1 and prop2 in the MyClass3_DocumentationLost class result in none of the documentation from MyInterface showing.

🙂 Expected behavior

In VSCode, in the MyClass3_DocumentationLost class, hovering over the following props should so the corresponding documentation from MyInterface:

  • prop1 -> "Documentation for prop1"
  • prop2 -> "Documentation for prop2"
@frank-weindel
Copy link
Author

I just realized a workaround for this, though I'm sure it's not great for performance:

//
// 4: Workaround by using `keyof Omit<>`
//
type MyModifiedInterface4_DocumentationPreserved = {
    [P in keyof Omit<MyInterface, 'prop3'>]: MyInterface[P];
};

class MyClass4_DocumentationPreserved implements MyModifiedInterface4_DocumentationPreserved {
    prop1 = 1; // Hover doc: Documentation for prop1
    prop2 = 'a'; // Hover doc: Documentation for prop2
}

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Experience Enhancement Noncontroversial enhancements labels Jul 29, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jul 29, 2022
@mindplay-dk
Copy link

See also #50715.

I believe this type of issue is why practically every UI library out there currently copies (or generates) and duplicates most of the type-information and documentation already present in HTMLElementTagNameMap and all the related types - copying the documentation is either too difficult, or in some cases likely not possible.

Building a typed UI library is a monumental effort - and updates to standard HTML element/attribute/event types (from official IDL definitions) really ought to come with built-in platform type definitions.

(This would be something that imperative types could potentially alleviate as well - imagine you could simply return { docs: relatedType.docs } and just copy the documentation from whatever type/member you want.)

@Peeja
Copy link
Contributor

Peeja commented Mar 31, 2023

Just to add some clarity while I have the information loaded in memory: The reason this happens is that in the checker's resolveMappedTypeMembers() it specifically checks to see if the mapped type uses keyof (isMappedTypeWithKeyofConstraintDeclaration()) and then pulls out the object type that's mapped over to get the original properties. Without doing that, there's no documentation information to use—the names of the properties on their own don't cary that documentation, only the properties themselves as they belong to their parent object type.

@Peeja
Copy link
Contributor

Peeja commented Jun 5, 2023

Ah, here we go! It has to be an in keyof to preserve the documentation, but you can still filter that using key remapping without breaking the documentation:

//
// 5: Filtered key remapping preserves documentation!
//
type MyModifiedInterface5_DocumentationPreserved = {
    [P in keyof MyInterface as Exclude<P, 'prop3'>]: MyInterface[P];
};

class MyClass5_DocumentationPreserved implements MyModifiedInterface5_DocumentationPreserved {
    prop1 = 1; // Hover doc: Documentation for prop1
    prop2 = 'a'; // Hover doc: Documentation for prop2
}

@Dimava
Copy link

Dimava commented Aug 4, 2023

keyof T constructs "property" type, which is linked to JSDoc and go-to-definition
To keep JSDoc and go-to-definition, you have to use need K in keyof ... as K clause

type ProperOmit<T, K extends PropertyKey> = {
    [P in keyof T as P extends K ? never : P]: T[P];
    // ~~~~~~~~~~~~~ P in keyof ... as P   ~
}

Omit uses Pick internally which does keep keys as they get casted to keyof T

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
    // P in <keyof T> as P
};

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

5 participants