-
Notifications
You must be signed in to change notification settings - Fork 708
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
Manually defining "Type Declarations" on a type alias #2032
Comments
Yep, you're correct there's no builtin way to do this today. There aren't really any plugins which do something like this either.... a first pass could look something like: // CC0
// @ts-check
const td = require("typedoc");
/** @param {td.Application} app */
exports.load = function (app) {
app.converter.on(td.Converter.EVENT_CREATE_DECLARATION, replaceObjectLikeTypes);
};
/**
* @param {td.Context} context
* @param {td.DeclarationReflection} reflection
*/
function replaceObjectLikeTypes(context, reflection) {
if (reflection.kind !== td.ReflectionKind.TypeAlias) return;
const propTags = reflection.comment?.getTags("@property");
if (!propTags?.length) return;
const symbol = context.project.getSymbolFromReflection(reflection);
const declaration = symbol?.declarations?.[0];
if (!symbol || !declaration) return; // Will probably never happen in practice
const reflectionType = context.checker.getDeclaredTypeOfSymbol(symbol);
const typeRefl = context
.withScope(reflection)
.createDeclarationReflection(td.ReflectionKind.TypeLiteral, undefined, undefined, "__type");
context.finalizeDeclarationReflection(typeRefl);
const typeContext = context.withScope(typeRefl);
for (const tag of propTags) {
if (!tag.name) {
context.logger.warn(`@property tag missing a name in ${reflection.getFriendlyFullName()}`);
continue;
}
const memberType = reflectionType?.getProperty(tag.name);
if (!memberType) {
context.logger.warn(
`${reflection.getFriendlyFullName()} specified '@property ${
tag.name
}', but does not have a property called '${tag.name}', will be documented as any.`
);
}
const element = typeContext.createDeclarationReflection(
td.ReflectionKind.Property,
undefined,
undefined,
tag.name
);
element.comment = new td.Comment(tag.content);
element.type = typeContext.converter.convertType(
typeContext,
memberType && context.checker.getTypeOfSymbolAtLocation(memberType, declaration)
);
typeContext.finalizeDeclarationReflection(element);
}
reflection.comment?.removeTags("@property");
reflection.type = new td.ReflectionType(typeRefl);
} When running with: typedoc src/gh2032.ts --plugin ./plugins/prop.js /**
* Foo docs
*/
export type Foo = {
/**
* Foo.a docs
*/
a: 123;
/**
* Foo.b docs
*/
b: 456;
};
/**
* Bar docs
* @property a docs
* @property b docs
* @property c could lie
*/
export type Bar = {
[K in keyof Foo]: string;
}; Prints:
|
That's brilliant, thank you! I'll spend some time playing with this plugin to learn how it all works. 🍺 |
Hiya 👋 Just wanted to share my updated solution here. I ended up going with a more dynamic approach that automatically expands type aliases of types Here's a simple example. // Input
export type Animal = {
/** The animal's name. */
name: string;
/** The animal's number of eyes. */
eyes: number;
}
export type Bird = Animal & {
/** The length of the bird's wings. */
wingspan: number;
}
// Output
export type Bird = {
/** The animal's name. */
name: string;
/** The animal's number of eyes. */
eyes: number;
/** The length of the bird's wings. */
wingspan: number;
} Here's a more complex example. // Input
export type Animal = {
/** The animal's name. */
name: string;
/**
* The animal's number of eyes.
* @defaultValue `2`
*/
eyes: number;
};
export type Messenger = {
/** The company used to send the message. */
company: string;
/** Send a message. */
send(message: string): void;
};
export type Bird = Partial<Animal> &
Omit<Messenger, 'company'> & {
/** The length of the bird's wings. */
wingspan: number;
};
// Output
export type Bird = {
/** The animal's name. */
name?: string;
/**
* The animal's number of eyes.
* @defaultValue `2`
*/
eyes?: number;
/** The length of the bird's wings. */
wingspan: number;
/** Send a message. */
send(message: string): void;
} And here's my implementation. // CC0
// @ts-check
const td = require('typedoc');
/** @param {td.Application} app */
exports.load = function (app) {
app.converter.on(
td.Converter.EVENT_CREATE_DECLARATION,
expandObjectLikeTypes
);
};
/**
* The reflection types affected by this plugin.
*/
const TYPES_TO_EXPAND = ['mapped', 'intersection', 'reference'];
/**
* @param {td.Context} context
* @param {td.DeclarationReflection} reflection
*/
function expandObjectLikeTypes(context, reflection) {
if (
reflection.kind !== td.ReflectionKind.TypeAlias ||
!reflection.type?.type ||
!TYPES_TO_EXPAND.includes(reflection.type.type)
)
return;
const symbol = context.project.getSymbolFromReflection(reflection);
const declaration = symbol?.declarations?.[0];
if (!symbol || !declaration) return; // Will probably never happen in practice
const reflectionType = context.checker.getDeclaredTypeOfSymbol(symbol);
const typeRefl = context
.withScope(reflection)
.createDeclarationReflection(
td.ReflectionKind.TypeLiteral,
undefined,
undefined,
'__type'
);
context.finalizeDeclarationReflection(typeRefl);
const typeContext = context.withScope(typeRefl);
for (const propertySymbol of reflectionType.getProperties()) {
const propertyType =
propertySymbol &&
context.checker.getTypeOfSymbolAtLocation(propertySymbol, declaration);
const resolvedReflection = resolvePropertyReflection(
context,
reflectionType,
propertySymbol
);
const element = typeContext.createDeclarationReflection(
td.ReflectionKind.Property,
undefined,
undefined,
propertySymbol.name
);
if (resolvedReflection) {
element.comment = resolvedReflection.comment;
element.flags = resolvedReflection.flags;
element.sources = resolvedReflection.sources;
element.url = resolvedReflection.url;
element.anchor = resolvedReflection.anchor;
element.cssClasses = resolvedReflection.cssClasses;
if (resolvedReflection instanceof td.DeclarationReflection) {
element.defaultValue = resolvedReflection.defaultValue;
}
}
element.type = typeContext.converter.convertType(typeContext, propertyType);
typeContext.finalizeDeclarationReflection(element);
}
reflection.type = new td.ReflectionType(typeRefl);
}
/**
* @param {td.Context} context
* @param {td.TypeScript.Type} objectType
* @param {td.TypeScript.Symbol} propertySymbol
*/
function resolvePropertyReflection(context, objectType, propertySymbol) {
const resolvedType = context.checker.getPropertyOfType(
objectType,
propertySymbol.name
);
const resolvedDeclaration = resolvedType?.declarations?.[0];
const resolvedSymbol =
resolvedDeclaration && context.getSymbolAtLocation(resolvedDeclaration);
return (
resolvedSymbol && context.project.getReflectionFromSymbol(resolvedSymbol)
);
} Thank you for engineering this really powerful plugin system. I hope this helps someone and please don't hesitate to let me know if there's a simpler way of achieving this. 😊 |
element.url = resolvedReflection.url;
element.anchor = resolvedReflection.anchor;
element.cssClasses = resolvedReflection.cssClasses; There's no point in doing this at this point, these haven't been set yet. (sources may have been set, I can never remember how event listeners get added, they've probably been set) if (resolvedReflection instanceof td.DeclarationReflection) {
element.defaultValue = resolvedReflection.defaultValue;
} Seems kind of weird to me to do this, since type aliases live only in type space and therefore can't have "real" default values, but to each their own. It's also worth mentioning that this plugin may produce different results for: export const x = { a: 1 }
export type Y = { [K in keyof typeof x]: string }
// vs
export type Y = { [K in keyof typeof x]: string }
export const x = { a: 1 } You could fix this by deferring the type replacement until |
Hi, thanks for the tips! Good shout, the FYI I've tried the provided examples and they both produced the same expected result. |
I know the concern raised by this issue has been plaguing you @Gerrit0 for at least a year or two now... maybe there's a workaround that would be easier to implement? I understand that an automated type resolution system would be most ideal for this use case... but what about leaning into this issue's use of the word "manually" in the title? I'm thinking something like: /**
* @param x DocsFriendlyX - input to function
*/
export function foo(x: X) {
/* ... */
}
// elsewhere
export type X = /* ... something complicated to look at ... */;
export type DocsFriendlyX = /* ... something not as complicated to look at ... */ No idea if this would be easier, but it seems relevant to bring up here... like, TypeDoc already knows all the references everywhere; seems like all that would be necessary here is a way to tell TypeDoc to forcibly override one type with another. Just a thought! Thanks! (quick edit: re-reading through this, it just dawned on me that maybe what I'm talking about here warrants a new issue. please advise if you'd like me to open this separately!) |
Huh, I thought ts gave exports in declaration order... must be more complicated than that. Might need to move one or the other into a separate file and re-export so that TypeDoc converts them in a specific order. (Direct exports are converted first)
Definitely a different feature request - and not something I'm likely to go for. TypeDoc is meant to document the API visible to your users, closely matching what they'll see in declaration files when actually using the library. If your exported API is so complicated that displaying that to a user isn't feasible... that might be an indication that a simpler design is warranted.. I would probably include a comment on |
Search terms
type alias, type declarations, inherit properties
Question
Hi there 👋
I've got a type alias
A
that depends on a simple object typeB
(usingOmit
and adding a bunch of other properties) and I would like the typeA
to be as thoroughly documented as typeB
.Here's a concrete example
B
isMetadata
which is an object type with some documented properties.Nft
type (typeA
in this analogy) uses most of the data ofMetadata
but removes some properties and add some new ones.Because type
A
is identified as a type alias, it doesn't get expanded (by design) but I would still like to add some tags on the docblocks to document it like typeB
.I've not been able to find any documented tags that serve that purpose. Ideally, I would have a
@property
tag that would fill the "Type declaration" section of the type instead of adding a new "Property" block. Even better, I would have a way to link a property from another type and inherit the docblock of that property only.I imagine there's no native way of achieving this right now but could I achieve this via a plugin?
I couldn't find much literature on how to create plugins so I would appreciate some pointers here.
Thank you for taking the time to read this. 🌺
The text was updated successfully, but these errors were encountered: