Way to expand complex type aliases (Awaited, ReturnType, typeof etc.)
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.
You're after @interface - https://typedoc.org/tags/interface/
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;
}[];
}
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);
});
}
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.
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.