TypeScript 在展开的Omit< >Map类型中,剩余属性的Intellisense文档丢失,

35g0bw71  于 6个月前  发布在  TypeScript
关注(0)|答案(9)|浏览(140)

Bug报告
我尝试在接口上使用Map类型,并希望在类实现Map接口时保留原始接口的属性文档。这通常没问题,除非Map器从原始接口中删除了一些键。下面的示例实际上表明,Omit<>Omit<> 的一级别扩展可以很好地保留剩余属性的文档。然而,如果我再将其展开到一个原始Map类型表达式中,那么我突然失去了文档。
不幸的是,我无法在试图编写的代码中使用前两个选项,因为我在Map的值侧进行条件类型检查。
谢谢大家!

🔎 搜索词

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

🕗 版本与回归信息

  • 这是我尝试的每个版本的行为,我也查阅了关于Map类型的FAQ条目

⏯ 实验场链接

带有相关代码的实验场链接

💻 代码

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!
}

🙁 实际行为

在VSCode中,将鼠标悬停在 MyClass3_DocumentationLost 类中的 prop1prop2 时,MyInterface中的任何文档都不会显示。

🙂 预期行为

在VSCode的 MyClass3_DocumentationLost 类中,将鼠标悬停在以下属性上应该显示 MyInterface 中的相应文档:

  • prop1 -> "prop1的文档"
  • prop2 -> "prop2的文档"
vjhs03f7

vjhs03f71#

一个例子是这样的:
假设你有一个简单的组件,比如一个标签,它需要一些属性。

export interface LabelProps {

  /** The label color. */
  readonly color?: string;

  /** The label text. */
  readonly text?: string;
}

export type WithLabelProps<TPrefix extends string> = {
  readonly [TKey in keyof LabelProps as `${TPrefix}${(TPrefix extends "") ? TKey : Capitalize<TKey>}`]?: LabelProps[TKey];
}

export function getLabelProps<TPrefix extends string>(prefix: TPrefix, props: WithLabelProps<TPrefix>): LabelProps {
  //... do conversion.
}

export function Label(props: LabelProps) {
  //... do stuff.
}

现在你有一个复合组件,它是由几个组件组成的,包括两个标签,你想在复合组件上公开这些属性,但要适当地加上前缀,以免冲突。

export interface CompositeComponentProps extends
  WithLabelProps<"firstLabel">,
  WithLabelProps<"secondLabel"> {
  
  readonly someProp?: string;  
}

export function CompositeComponent(props: CompositeComponentProps) {
 
  return (
    <>
      <Label {...getLabelProps("firstLabel", props)} />
      <Label {...getLabelProps("secondLabel", props)} />  
    </>
  );
}

然后在一个消费复合组件的组件中,你可以这样做:

export function MyComponent() {
  return (
    <CompositeComponent
      firstLabelText="Label 1"
      secondLabelText="Label 2" />
  );
}

在这种情况下,如果 firstLabelTextsecondLabelText 上有 LabelProps["text"] 的文档就更好了。这确实意味着确保你创建的文档足够通用,但它允许我在一个地方定义我的属性(与它们所属的组件共存),然后让任何额外的新属性自动过滤到暴露其组成成分属性的组件(例如,一个按钮可能有一个标签、一个图标和一个工具提示,我想让按钮自动公开底层组件的属性)。
希望这样就够了。

qmb5sa22

qmb5sa222#

啊!是的,我认为这是一个很好的例子,可以重用原始文档并使其有用。

9nvpjoqh

9nvpjoqh3#

我刚刚找到了一个解决这个问题的方法,虽然我知道它对性能可能不是很好:

//
// 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
}
gpnt7bae

gpnt7bae4#

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.)

rpppsulh

rpppsulh5#

为了在内存中加载信息时增加一些清晰度:这种情况发生的原因是在 checkerresolveMappedTypeMembers() 中,它专门检查Map类型是否使用了 keyof(isMappedTypeWithKeyofConstraintDeclaration()),然后提取Map到的对象类型以获取原始属性。如果不这样做,就没有文档信息可以使用——属性本身的名字本身并不包含文档信息,只有它们作为属于其父对象类型的一部分时才包含文档信息。

bqf10yzr

bqf10yzr6#

啊,我们开始吧!为了保留文档,它必须是一个in keyof,但你仍然可以使用键Map来过滤它,而不破坏文档:

//
// 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
}
piztneat

piztneat7#

keyof T 构造 "property" 类型,与 JSDoc 和 go-to-definition 相关联
为了保持 JSDoc 和 go-to-definition,您需要使用 K in keyof ... as K 子句。

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

Omit 内部使用了 Pick,这会将键保留为它们被转换为 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
};
kdfy810k

kdfy810k8#

问题是,如果你使用键Map重写键名,你将失去与原始引用属性关联的文档:
例如:

interface Source {

  /** Some documentation */
  readonly prop1: string;
} 

type Dest1 ={
  readonly [TKey in keyof Source]: Source[TKey];
}

type Dest2 = {
  readonly [TKey in keyof Source as `${TKey}`]?: Source[TKey];
}

const x: Dest1;
const y: Dest2;

如果你在 x 上触发 intellisense,你会看到保留了文档的 prop1,但是如果你在 y 上触发它,你仍然会看到 prop1,但原始源文档丢失了。我认为这可能是一个bug,因为我希望与 in keyof Source 关联的文档在任何情况下都能应用,而不受名称重写的影响。
我经常使用这种模式来允许为传递给子组件的属性添加前缀,例如在React中,但是文档丢失的事实很糟糕,我更希望保留文档。

jtw3ybtb

jtw3ybtb9#

你有没有一个实际的例子可以提供?我的假设是,在几乎所有的情况下,如果你正在重命名键,原始文档不再完全适合。但如果有一种方法可以将它插值到Map键的新JSDoc注解中,那将是非常棒的。

相关问题