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

Way to expand complex type aliases (Awaited, ReturnType, typeof etc.) #2654

Closed
typeofweb opened this issue Aug 4, 2024 · 5 comments
Closed
Labels
enhancement Improved functionality
Milestone

Comments

@typeofweb
Copy link

Search Terms

Awaited, ReturnType, typeof

Problem

I often have types that rely on the implementation, i.e.:

export type Product = Awaited<ReturnType<typeof getProduct>>;

Currently, the documentation generated for such type is:

Product: Awaited<ReturnType<typeof getProduct>>

It's not instantly useful to the users.

Suggested Solution

Expand the type to the extent that the TypeScript language server does by default. In this case, it shows an object – all aliases are squashed.

@typeofweb typeofweb added the enhancement Improved functionality label Aug 4, 2024
@Gerrit0
Copy link
Collaborator

Gerrit0 commented Aug 4, 2024

You're after @interface - https://typedoc.org/tags/interface/

@typeofweb
Copy link
Author

Indeed, I missed @interface! Thank you.

Unfortunately, it behaves somewhat unexpectedly in the case when a function returns an array. For example:

function doSth() {
  return [{ abc: 123 }];
}
/** @interface */
export type DoSth = ReturnType<typeof doSth>;

Expected:

type DoSth = {
    abc: number;
}[]

Actual:

interface DoSth {
    [unscopables]: {
        [unscopables]?: boolean;
        length?: boolean;
        [iterator]?: any;
        at?: any;
        concat?: any;
        copyWithin?: any;
        entries?: any;
        every?: any;
        fill?: any;
        filter?: any;
        find?: any;
        findIndex?: any;
        findLast?: any;
        findLastIndex?: any;
        flat?: any;
        flatMap?: any;
        forEach?: any;
        includes?: any;
        indexOf?: any;
        join?: any;
        keys?: any;
        lastIndexOf?: any;
        map?: any;
        pop?: any;
        push?: any;
        reduce?: any;
        reduceRight?: any;
        reverse?: any;
        shift?: any;
        slice?: any;
        some?: any;
        sort?: any;
        splice?: any;
        toLocaleString?: any;
        toReversed?: any;
        toSorted?: any;
        toSpliced?: any;
        toString?: any;
        unshift?: any;
        values?: any;
        with?: any;
    };
    length: number;
    [iterator](): IterableIterator<{
        abc: number;
    }>;
    at(index: number): undefined | {
        abc: number;
    };
    concat(...items: ConcatArray<{
        abc: number;
    }>[]): {
        abc: number;
    }[];
    concat(...items: ({
        abc: number;
    } | ConcatArray<{
        abc: number;
    }>)[]): {
        abc: number;
    }[];
    copyWithin(target: number, start: number, end?: number): this;
    entries(): IterableIterator<[number, {
        abc: number;
    }]>;
    every<S>(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => value is S), thisArg?: any): this is S[];
    every(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => unknown), thisArg?: any): boolean;
    fill(value: {
        abc: number;
    }, start?: number, end?: number): this;
    filter<S>(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => value is S), thisArg?: any): S[];
    filter(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => unknown), thisArg?: any): {
        abc: number;
    }[];
    filter(predicate: BooleanConstructor, thisArg?: unknown): {
        abc: number;
    }[];
    find<S>(predicate: ((value: {
        abc: number;
    }, index: number, obj: {
        abc: number;
    }[]) => value is S), thisArg?: any): undefined | S;
    find(predicate: ((value: {
        abc: number;
    }, index: number, obj: {
        abc: number;
    }[]) => unknown), thisArg?: any): undefined | {
        abc: number;
    };
    findIndex(predicate: ((value: {
        abc: number;
    }, index: number, obj: {
        abc: number;
    }[]) => unknown), thisArg?: any): number;
    findLast<S>(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => value is S), thisArg?: any): undefined | S;
    findLast(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => unknown), thisArg?: any): undefined | {
        abc: number;
    };
    findLastIndex(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => unknown), thisArg?: any): number;
    flat<A, D>(this: A, depth?: D): FlatArray<A, D>[];
    flatMap<U, This>(callback: ((this: This, value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => U | readonly U[]), thisArg?: This): U[];
    forEach(callbackfn: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => void), thisArg?: any): void;
    includes(searchElement: {
        abc: number;
    }, fromIndex?: number): boolean;
    includes(searchElement: unknown, fromIndex?: number): searchElement is {
        abc: number;
    };
    indexOf(searchElement: {
        abc: number;
    }, fromIndex?: number): number;
    join(separator?: string): string;
    keys(): IterableIterator<number>;
    lastIndexOf(searchElement: {
        abc: number;
    }, fromIndex?: number): number;
    map<U>(callbackfn: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => U), thisArg?: any): U[];
    pop(): undefined | {
        abc: number;
    };
    push(...items: {
        abc: number;
    }[]): number;
    reduce(callbackfn: ((previousValue: {
        abc: number;
    }, currentValue: {
        abc: number;
    }, currentIndex: number, array: {
        abc: number;
    }[]) => {
        abc: number;
    })): {
        abc: number;
    };
    reduce(callbackfn: ((previousValue: {
        abc: number;
    }, currentValue: {
        abc: number;
    }, currentIndex: number, array: {
        abc: number;
    }[]) => {
        abc: number;
    }), initialValue: {
        abc: number;
    }): {
        abc: number;
    };
    reduce<U>(callbackfn: ((previousValue: U, currentValue: {
        abc: number;
    }, currentIndex: number, array: {
        abc: number;
    }[]) => U), initialValue: U): U;
    reduceRight(callbackfn: ((previousValue: {
        abc: number;
    }, currentValue: {
        abc: number;
    }, currentIndex: number, array: {
        abc: number;
    }[]) => {
        abc: number;
    })): {
        abc: number;
    };
    reduceRight(callbackfn: ((previousValue: {
        abc: number;
    }, currentValue: {
        abc: number;
    }, currentIndex: number, array: {
        abc: number;
    }[]) => {
        abc: number;
    }), initialValue: {
        abc: number;
    }): {
        abc: number;
    };
    reduceRight<U>(callbackfn: ((previousValue: U, currentValue: {
        abc: number;
    }, currentIndex: number, array: {
        abc: number;
    }[]) => U), initialValue: U): U;
    reverse(): {
        abc: number;
    }[];
    shift(): undefined | {
        abc: number;
    };
    slice(start?: number, end?: number): {
        abc: number;
    }[];
    some(predicate: ((value: {
        abc: number;
    }, index: number, array: {
        abc: number;
    }[]) => unknown), thisArg?: any): boolean;
    sort(compareFn?: ((a: {
        abc: number;
    }, b: {
        abc: number;
    }) => number)): this;
    splice(start: number, deleteCount?: number): {
        abc: number;
    }[];
    splice(start: number, deleteCount: number, ...items: {
        abc: number;
    }[]): {
        abc: number;
    }[];
    toLocaleString(): string;
    toLocaleString(locales: string | string[], options?: NumberFormatOptions & DateTimeFormatOptions): string;
    toReversed(): {
        abc: number;
    }[];
    toSorted(compareFn?: ((a: {
        abc: number;
    }, b: {
        abc: number;
    }) => number)): {
        abc: number;
    }[];
    toSpliced(start: number, deleteCount: number, ...items: {
        abc: number;
    }[]): {
        abc: number;
    }[];
    toSpliced(start: number, deleteCount?: number): {
        abc: number;
    }[];
    toString(): string;
    unshift(...items: {
        abc: number;
    }[]): number;
    values(): IterableIterator<{
        abc: number;
    }>;
    with(index: number, value: {
        abc: number;
    }): {
        abc: number;
    }[];
}

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Aug 5, 2024

That's working as expected -- it is accurately describing an Array<{ abc: number }> as an interface, which is what the @interface tag says to do.

TypeDoc doesn't have a tag to document a type alias using the "hovered type"... perhaps it should. I've avoided it so far as it is usually an indication that the code being documented would likely be improved by some other simplification. I do keep circling back to this every few months though, so should probably just include it at some point...

It is easy to write a plugin which implements this functionality for now:

// CC0
// typedoc --plugin ./path/to/plugin.js
// @ts-check
import td, { ReflectionKind } from "typedoc";

const TAG = "@useHoverType";

/** @param {td.Application} app */
export function load(app) {
    // Automatically add the tag to the supported list of modifier tags
    app.on(td.Application.EVENT_BOOTSTRAP_END, () => {
        const tags = [...app.options.getValue("modifierTags")];
        if (!tags.includes(TAG)) {
            tags.push(TAG);
        }
        app.options.setValue("modifierTags", tags);
    });

    app.converter.on(td.Converter.EVENT_CREATE_DECLARATION, (context, decl) => {
        const symbol = context.project.getSymbolFromReflection(decl);

        if (!decl.kindOf(ReflectionKind.TypeAlias) || !decl.comment?.hasModifier(TAG) || !symbol) {
            return;
        }

        decl.comment.removeModifier(TAG);
        const type = context.checker.getDeclaredTypeOfSymbol(symbol);
        decl.type = context.converter.convertType(context.withScope(decl), type);
    });
}

@Oblarg
Copy link

Oblarg commented Aug 6, 2024

Is there a better semantic name for this than "hover type?" Or, if not a name - is there a good description of the semantics used for determining how hover tooltips are expanded? I'd be hesitant to use this for fear of it not being stable.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Aug 7, 2024

Oh, it's definitely not stable. It's entirely dependent on whatever TypeScript decides to do - and that can and has changed between TS versions. @useDeclaredType might be reasonable given that's the TS method to get the type... That said, it's good enough for most things. Every change I've seen to it has been a net improvement.

@Gerrit0 Gerrit0 added this to the v0.27.0 milestone Sep 29, 2024
Gerrit0 added a commit that referenced this issue Sep 29, 2024
@Gerrit0 Gerrit0 closed this as completed Sep 29, 2024
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

3 participants