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

Add docstring representation for inferred parameters #2551

Open
Oblarg opened this issue Apr 18, 2024 · 8 comments
Open

Add docstring representation for inferred parameters #2551

Oblarg opened this issue Apr 18, 2024 · 8 comments
Labels
enhancement Improved functionality

Comments

@Oblarg
Copy link

Oblarg commented Apr 18, 2024

Search Terms

generic parameters
type parameters

Problem

Inferred types (i.e. those following infer keyword) are colored as type parameters, but there is no way to document them. It is sometimes necessary to provide explanation for inferred types, since they can feature directly in a typedef or in the return type of a documented function.

Suggested Solution

A new section below "type parameters" should be added specifically for inferred types, with a corresponding docstring annotation (possibly @inferredType or just @inferred).

@Oblarg Oblarg added the enhancement Improved functionality label Apr 18, 2024
@Gerrit0
Copy link
Collaborator

Gerrit0 commented Apr 18, 2024

I'm not sure this is a good idea.

Type parameters which are internal to the implementation of your type are exactly that. Internal implementation details.

So far as the users of the following types are concerned... these are equivalent. Whether a utility delegates to a helper or performs the work inline isn't relevant.

type GetParamsAndReturns<T extends (..._: any) => any> = { params: Parameters<T>; return: ReturnType<T> }
type GetParamsAndReturns<T extends (..._: any) => any> = T extends (..._: infer P) => infer R ? { params: P; return: R } : never

Further, these names are not necessarily unique, they're scoped to the conditional which introduces them, so documenting them as a part of the type alias feels very similar to documenting local variables within a function as part of that function's contract...

type Values<T> =
  T extends Array<infer U> ? U :
  T extends Map<string, infer U> ? U :
  never

You could do this with a custom plugin, but at this point I don't think it's something I want to include in TypeDoc itself.

@Oblarg
Copy link
Author

Oblarg commented Apr 18, 2024

An inferred type param isn't always an internal implementation detail, though - it can occur directly in the return type, and in those cases it's important for the user to know the bounds on what was inferred. If it is possible to render the result of the typedef such that it can be understood without reference to the inferred variable, so much the better, but often i find this isn't the case.

Perhaps they should only be rendered if they're given a @typeParam annotation?

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Apr 19, 2024

From that description, it sounds like what you actually want is @returns (@typeReturns?) for type aliases, re-using @typeParam for it seems like a mixing of concerns with input/output and a recipe for more confusion

@Oblarg
Copy link
Author

Oblarg commented Apr 19, 2024

@typeReturns would be more accurate, yeah, but upon reflection these should probably just get their own block. I've edited the feature request accordingly.

@Oblarg Oblarg changed the title Add representation for inferred parameters in type parameter list Add docstring representation for inferred parameters Apr 19, 2024
@Gerrit0
Copy link
Collaborator

Gerrit0 commented Apr 27, 2024

I don't think this needs special handling in TypeDoc, unless I'm missing something...

/**
 * @typeReturns An object describing the parameters and return type of the function
 */
export type GetParamsAndReturns<T extends (..._: any) => any> = { params: Parameters<T>; return: ReturnType<T> }

Renders as:

image

@Oblarg
Copy link
Author

Oblarg commented Apr 27, 2024

See the amended request - I still think there should be a bespoke section for inferred types that is formatted like the type parameter section and has similar hyperlinking behavior. Describing the return type can sidestep the problem for functions, but I think it's far from optimal for highly-algebraic types, especially in internal developer-facing docs. Consider:

export type PropagatedEvent<SourceTopic, TargetTopic, Event> = Event extends infer E extends { 
        type: string; 
    }
    ? Omit<E, "type" | "topic"> & { 
            topic: TargetTopic; 
            type: `${SourceTopic}-${E["type"]}`; 
        }
    : never

Here E is specifically a member of the union type Event - understanding this helps greatly in making sense of the type - but there's no natural place to document this in TypeDoc as-is. Note that this isn't a function, it's a typedef, so there's no "returns" involved. Since the inference behavior is part of the contract of the type, I feel that it should be represented.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Apr 27, 2024

I think it is "far from optimal for highly-algebraic types" because many metaprogramming types like that suffers from lack of encapsulation where every "type-function" (aka typedef - it is a type which operates on a type and returns a type...) needs to be understood in order to use it. Personally, if writing documentation for types like this I'd make every effort to let users of the types not need to read the type, including myself in 6 months. A good example of this is Awaited.

Consider TypeScript's documentation for Awaited<T>...

This type is meant to model operations like await in async functions, or the .then() method on Promises - specifically, the way that they recursively unwrap Promises.

type A = Awaited<Promise<string>>;
//   ^? - string
type B = Awaited<Promise<Promise<number>>>;
//   ^? - number
type C = Awaited<boolean | Promise<number>>;
//   ^? - number | boolean

That's all you really need to know in order to use the type... it doesn't say anything about how the type works under the hood, because in almost every case, you don't care! It also includes a few examples to let potential users of the type quickly get an understanding of what the type does for them.

Then compare that to the implementation

type Awaited<T> =
    T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
        T extends object & { then(onfulfilled: infer F): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
            F extends ((value: infer V) => any) ? // if the argument to `then` is callable, extracts the argument
                Awaited<V> : // recursively unwrap the value
                never : // the argument to `then` was not callable
        T; // non-object or non-thenable

I think this approach, where inline comments are used for anyone who needs to peek behind the curtain, and a high level description + list of examples for most users is the best way to document complex types.

TypeDoc could try to provide more features around making explaining these types easier, but I doubt it will ever be able to provide a better experience to exploring such a type than your editor can...

@Oblarg
Copy link
Author

Oblarg commented Apr 28, 2024

The users already do not need to read the type - it has a high-level description, as do the types that rely on it, and the overall behavior of the type is understandable without parsing the entire definition. But I like to offer as much as I reasonably can to a developer without forcing them to context-switch, and this is information that I'd like to include in our internal developer-facing builds of the API docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improved functionality
Projects
None yet
Development

No branches or pull requests

2 participants