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

Parent class referenced via symbol of type "typeof Parent" not included in class hierarchy #2775

Closed
blutorange opened this issue Nov 14, 2024 · 6 comments
Labels
design-limitation enhancement Improved functionality

Comments

@blutorange
Copy link

Search Terms

class hierarchy typeof parent

Problem

A very simple example:

declare class A {}
declare class B extends A {}
declare const RefToA: typeof A;
declare class C extends RefToA {}

In the generated documentation,

  • C has RefToA as a parent
    image
  • RefToA is just typeof A and you can click on it
    image
  • But the hierarchy of A only includes B, not C
    image

Below is a more elaborate example that shows what we're trying to do. The project consist of multiple different script files (with implicit dependencies) and linking between different script files is done via the global scope. So you you code like class InputText extends PrimeFaces.widget.BaseWidget primefaces/primefaces#12717

Suggested Solution

Perhaps #2467 is related, but this seems like a different issue. I don't know how hard or easy it is to get class hierarchy information form TypeScript? Naively, I would imagine querying TypeScript for the type of the extends expression and checking if that is a known class.

More elaborate example

declare class BaseWidget {
    readonly base: string;
}

declare class DeferredWidget  extends BaseWidget {
    readonly deferred: string;
}

declare class InputTextArea extends PrimeFaces.widget.BaseWidget {
    readonly inputtextarea: string;
}

interface PrimeFaces {
    widget: PrimeType.widget.WidgetRegistry;
}

declare namespace PrimeType.widget {
    interface WidgetRegistry {
        BaseWidget: typeof BaseWidget;
        InputTextArea: typeof InputTextArea;
    }
}

declare let PrimeFaces: PrimeFaces;
@blutorange blutorange added the enhancement Improved functionality label Nov 14, 2024
@Gerrit0
Copy link
Collaborator

Gerrit0 commented Nov 15, 2024

This is going to be a wontfix/design limitation. TypeDoc relies on the symbol to link parent/children classes and properties/variables introduce a new symbol which cannot be reliably linked to the real class.

It also isn't generally safe to rely on typeof X meaning "the class X"

@blutorange
Copy link
Author

Hmm, ok, thanks for confirming. Correct me if I'm mistaken, there's no other way in TypeScript to indicate that some property holds a class (so that TypeDoc would be able to reference it), right?

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Nov 15, 2024

Without introducing a new symbol, I'm not aware of any.

If you don't care about the safety issue, you could use a plugin to re-point references to the property to the originating class...

npx typedoc src/gh2775.d.ts --plugin ./plugins/gh2775.js
// CC0
import td, { Converter, ReflectionKind } from "typedoc";

/** @param {td.Application} app */
export function load(app) {
    app.converter.on(
        Converter.EVENT_RESOLVE_BEGIN,
        (context) => {
            for (const decl of context.project.getReflectionsByKind(ReflectionKind.ClassOrInterface)) {
                hackInheritance(decl);
            }
        },
        1e6,
    );
}

/** @param {td.DeclarationReflection} decl */
function hackInheritance(decl) {
    if (!decl.extendedTypes) return;

    for (let i = 0; i < decl.extendedTypes.length; ++i) {
        const type = decl.extendedTypes[i];
        if (type.type !== "reference") continue;
        if (!type.reflection) continue;

        /** @type {td.DeclarationReflection} */
        const target = type.reflection;

        if (target.kindOf(ReflectionKind.ClassOrInterface)) continue;

        const targetType = target.type;
        if (targetType.type !== "query") continue;
        if (!targetType.queryType.reflection?.kindOf(ReflectionKind.ClassOrInterface)) continue;

        // We inherit from a property somewhere which is declared as `typeof SomeClass`
        // So re-point the inheritance to SomeClass
        decl.extendedTypes[i] = targetType.queryType;
    }
}

blutorange pushed a commit to blutorange/primefaces-js-refactor-test that referenced this issue Nov 16, 2024
@blutorange
Copy link
Author

Yeah I thought so. Thanks again for the quick reply : ) That hack does what it's intended to do.

If you don't care about the safety issue

I do care, in general, about (type) safety (but also about nice docs for users). If you don't mind me asking, what case did you have in mind where replacing typeof X is unsafe? Anyways, I think we can close this issue for now, since it does not seem to be possible in general.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Nov 16, 2024

I guess "safety" is a somewhat confusing term to use. The issue I see with including this in typedoc itself is:

class Thing {
  method() { return "a" }
}

class OtherThing {
  method() { return "b" }
}

namespace Registry {
  const Thing: typeof Thing = Math.random() > 0.5 ? Thing : OtherThing;
}

// Docs shouldn't mark Child as inheriting from Thing
class Child extends Registry.Thing {}

@blutorange
Copy link
Author

I see, that's a good point, TypeScript doesn't have nominal types, so it considers classes with the same structure as equivalent. That's probably not an issue in most practical cases, but I agree, it's not correct in general.

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

No branches or pull requests

2 participants