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

JavaScript class decorators returning extended class that implements the target but types can't match the constraints #58022

Closed
zthun opened this issue Apr 1, 2024 · 3 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@zthun
Copy link

zthun commented Apr 1, 2024

🔎 Search Terms

  • incorrectly extends base class 'T'
  • is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint
  • Decorators
  • typeof HTMLElement

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about Decorators

⏯ Playground Link

https://www.typescriptlang.org/play?experimentalDecorators=true&emitDecoratorMetadata=true&ts=5.4.3&downlevelIteration=true#code/C4TwDgpgBAwg9gWzHAdhFx4oM7AE4CuAxsHHgDwAqAogDYQLrBQQAew6AJtlABKUBZADJ0GTAHxQAvFADeAKChQ0AdygAKAHTaAhngDm2AFxQdKEAG0AugEoTNeowwBuRVDB44pUJHuinwK4Avq7yAGYEKCQAlqiwiMhoGABKEPrRuBB46sA6+ia4eNEo+jZybngQwAR4KFARUcCxdeQwLOxcPD4QcGF8giKOEjl6+lUmMGUKSkrRfepEBLiI-kzYmmPAI6XSMigEtLRTbjNQi8sIqxjrnBBhxRDbADRQuQZVNq6nQSdQldW1V6jKpfKAheQ-cKRGJxeBIVBMADKAAsdJw4CoqO0OChuP1hFdgOJ1MclP8anUGjCWgBpbGdeLwpKYVCFYikCiUcTEt6bEw0uyMxJMLBskhkcj8AlDDCSaYzcmAoi0HTYHgAfThwowKLRGPpuK6wOY8tOYAIACNaNEiGdWfh2WQtLoDMZTOZrKTTjNsARINltJpRthPvJfqc5hoAITAZEZTTYVHolTJOBeL3emax+M6YC5IjI3XJ9SyKAIOC3EwAcjgkBQVbBoczSh+mdbM3b4Mh8gAAlqESk0hkONkqwgQABac6kBATiAy4BVmy9-vMosYknyZWqngwJYzwkGvFSwZiDByIJAA

💻 Code

// A single line here also somewhat reproduces the problem, but I'm not sure if TypeScript is capable of doing this kind of inference.  The constraint message is technically correct, but it is the point of what I'm trying to accomplish.  See playground for a more complete example

/*
Errors with Interface 'ComponentShadow<T>' incorrectly extends interface 'T'.
'ComponentShadow<T>' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'HTMLElement'.ts(2430)
*/

interface ComponentShadow<T extends HTMLElement> extends T {
    shadowRoot: ShadowRoot;
}

🙁 Actual behavior

I get an error: Interface incorrectly extends interface 'T'. is assignable to the constraint of type 'T', but 'T' could be instantiated with a different sub type of constraint HTMLElement (2430)

🙂 Expected behavior

I would hope that TypeScript would be able to detect that the return type from a constructor function that guarantees a type would know that an object extending that type would have it in the prototype chain.

Additional information about the issue

No response

@RyanCavanaugh
Copy link
Member

Subclassing doesn't guarantee subtyping. Anything declared in the derived class might illegally override something in the base class

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Apr 1, 2024
@zthun
Copy link
Author

zthun commented Apr 1, 2024

@RyanCavanaugh So how would that work in this case? I basically want to have the input target class be of type HTMLElement or any sub class of it and then extend that target.

interface Aspect {
     feature(): void;
}

type Constructor<T extends HTMLElement> = { 
   new(...args: any[]): T;
   prototype: T
}

// MyDecorator here should be allowed on any class that extends from HTMLElement
function MyDecorator<T extends HTMLElement>() {
    // Side note, I'm returning Constructor<T & Aspect> here which should be the correct return type,
    // but I have to replace this with any unless I want to prevent myself from using static properties 
    // in a class that uses this - which I believe is this issue here:  https://github.com/microsoft/TypeScript/issues/4881
    // That's fine.  I'm not worried about that since I can just use interface merging to get around that problem.
    return function (target: Constructor<T>): Constructor<T & Aspect> {
        // @ts-expect-error - I have to put this tag here in order to get this to work.
        return class _MyDecorator extends target implements Aspect {
            public feature() {
                console.log('Some new thing that gets added to target via sub classing it as a new prototype in front');
            }
        }
    }
}

// MyComponent extends a div element (using is="my-component")
// This does work as expected as long as I ignore the TypeScript error with //@ts-ignore or //@ts-expect-error.
@MyDecorator()
class MyComponent extends HTMLDivElement {
}

If this isn't possible, or a duplicate of the myriad of issues defined in #4881, then we can close this and I'll just use the workarounds I've found.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Apr 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

3 participants