microsoft / rushstack

Monorepo for tools developed by the Rush Stack community
https://rushstack.io/
Other
5.95k stars 602 forks source link

[api-extractor] TypeScript Advanced Types aren't expanded #1075

Open craigkovatch opened 5 years ago

craigkovatch commented 5 years ago

I have a TypeScript interface:

/** @public */
export interface SingleSelectDropdownProps extends Omit<DropdownButtonProps, 'aria-haspopup' | 'focusOnClose' | 'onKeyPress'> {
  /** The List to render in the dropdown popup. n.b. `onSelect` and `selectOnEnterKey` will be overridden.
   * In many cases it will be appropriate to set `list.selectedValue` equal to `value`. */
  list?: React.ReactElement<ListProps>;

  onChange?: (value: string) => void;

  /** the value of the currently selected item, if any. Must match the value of the respective item in `list.props.items`.
   * If `children` is not provided, `value` will also be rendered as the contents of the dropdown button. */
  value?: string;
}

The rendered JSON ouput after running api-extractor does not include any of the properties from the extended interface. Is this expected? image

Omit is defined like this: export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

But this problem occurs even with interfaces which extend only advanced types defined in the TS standard library, e.g.Pick:

(lib.es5.dom.ts)
/**
 * From T pick a set of properties K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
octogonz commented 5 years ago

This is currently by design: The .api.json file stores only the members that are explicitly declared with the type (otherwise e.g. each child class would need duplicates of all members of its entire parent chain, which would bloat the .api.json file).

If we want the API reference web site to show inherited members, for simple types this feature could perhaps be implemented in a documentation generator such as api-documenter. During generation the tool could examine the base classes/interfaces and add aliases for those members to the child class. We would probably accept such a PR for api-documenter, although I'd prefer for it to be an opt-in feature for now (since not everyone wants this behavior).

(I'll mention an important limitation, though: If the base class comes from some library that does not have a corresponding .api.json file, then there would be no way to discover those members from the .api.json files. In particular, if it's from an ambient declaration, it may be difficult to even produce an appropriate .api.json file, since today API Extractor does not attempt to analyze ambient declarations at all.)

Advanced types such as Omit cannot be processed in this way, however. The type algebra is too complicated for api-documenter and would need to be processed by api-extractor while it still has the full TypeScript compiler state. Since the members themselves are declared elsewhere, perhaps we would want the .api.json file to store these inherited members as links to the type that they were mapped from (assuming that type appears in the .api.json file). I haven't thought a lot about how that would work. Our own APIs try to avoid "PhD type algebra" because it's intimidating for newcomers and produces types that are difficult to describe in English prose. (I personally see React as a special exception, since React was designed without TypeScript in mind, and thus the type algebra has to do a lot of extra effort to try to model it.)

craigkovatch commented 5 years ago

Our own APIs try to avoid "PhD type algebra" because it's intimidating for newcomers and produces types that are difficult to describe in English prose.

I like this goal and I agree that the type algebra can get a little heady for me when I try to read the TS docs. That said, Pick and Omit are quite common and not too hard to understand, compared to the algebraic operators like | and &, conditional types, etc.

Maybe there's not a lot of implementation middle-ground, but at Tableau we use Pick and Omit quite extensively in even our relatively-simple UI library. I'm sad to say api-extractor is then a non-starter for us without the ability to expand these types.

So, yes, please do consider this an enthusiastic feature request. We're trying to get a better handle on our sprawling JS/TS codebase, and versioning/dependency management is still a major pain point in that area. So I'm really, really excited about this project (as are a few of my teammates). This issue is one of the last major impediments for us.

octogonz commented 5 years ago

Thanks! BTW if you'd like to help get a proposal started for how the .api.json file might represent these inherited members, that would be really helpful. If the design looks easy, someone might come along and implement it. :-)

BTW if the docs don't meet your current requirements, the .d.ts rollup and review file scenarios might still be relevant. They should handle Omit just fine, at least as of version 7.

pedroteixeira commented 2 years ago

Hi, is there any way for api-documenter to expand on complex utility types?

example, when using something like:

export interface A {
  getX(): number
}

export interface B {
   x:  ReturnType<A['getX']>
}

I would like to see documented the inferred type of x, not the utility type, i.e. interface B { x: number }

Any way to accomplish these?