microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.07k stars 12.49k forks source link

Add a new type of class declaration to support mixins #60254

Open AFatNiBBa opened 4 weeks ago

AFatNiBBa commented 4 weeks ago

🔍 Search Terms

"mixin", "InstanceType", "typeof", "class"

✅ Viability Checklist

⭐ Suggestion

When I run a mixin function, it outputs just the value of the returned expression, not the type

function myMixin<T extends new (...args: any[]) => any>(ctor: T) {
    return class extends ctor {
        a = 1;
    }
}

class MyClass {
    b = 2
}

const Result = myMixin(MyClass);

//             ↓ Result refers to a value, but is being used as a type here. Did you mean typeof Result ?          ↓ 
const istance: Result = new Result();

So I would need to define the type too

// ...
const Result = myMixin(MyClass);
type Result = InstanceType<typeof Result>;
// ...

I suggest adding a new syntax to define both a variable and a type with the same name

// ...
const Result = myMixin(MyClass) as type; // "as class" is also a viable option
// ...

Examples:

namespace Something {
    export class Idk {
        c = 1;
    }
}

const a = Something.Idk as type; // Ok

function someFunc() {
    return class {
        e = 1;
    }
}

const b = someFunc() as type; // Ok
const c = 1 as type; // Error: Expression "1" doesn't have any type attached

⭐ OPTIONAL extra generalised suggestion

Additionally, it would be nice if we could be able to generalise this process for all variables that also have an attached type

const a = 1;
type a = 2; // The type is unrelated to the value, no usage of this comes to my mind at the moment, but it already works

const b = a;
type b = a;
// Or
const b = a as type; // Defines a value from the `a` variable and a type from the `a` type

But it wouldn't work out of the box for the main suggestion, since Result doesn't actually have any type attached it would require functions (myMixin() for example) to be able to have attached return types

📃 Motivating Example

(Mentioned on the suggestion body)

💻 Use Cases

What do you want to use this for?

To improve the development of mixins in general

What shortcomings exist with current approaches?

Too verbose and feels like something it should to by default

const Result = myMixin(MyClass);
type Result = InstanceType<typeof Result>;

Creates an actual runtime class, that surely adds an overhead, althought it's minimal

class Result extends myMixin(MyClass) { }

What workarounds are you using in the meantime?

The runtime class one

jcalz commented 4 weeks ago

As written this is a runtime feature and will be declined. (You want class X = f() to be transformed into const X = f() at runtime)

You might want to consider changing the request so that the runtime portion would just need type erasure, like maybe const X = f() as class? Or something like that. I think I’ve seen such requests before though. Have to search for it to see if I find a duplicate. Relevant results:

18942 asks for this to happen automatically for all class constructor values, but this was declined. Also #18967. Also #45013. This isn't a duplicate of those because it looks like you're asking for this to be opt-in behavior instead of automatically.

AFatNiBBa commented 3 weeks ago

Sorry for that, I didn't realise you guys meant that by "runtime feature", but I get how this is a problem since you can't just erase the TypeScript specific syntax when compiling. As an alternative syntax I think it would be better to do something like this

const a = 1;
type a = 2;

//             ↓ "type" instead of "class", so that it would make sense even in more generic cases like this one that doesn't involve classes at all
const b = a as type;

I think that making this thing opt-in is quite important as it would probably do a lot of unwanted things otherwise

jcalz commented 3 weeks ago

(I'm not "you guys" exactly, just an interested bystander.)

Wait, why are you expanding the request to non-constructors? What are you expecting the type b to be there? 1? 2? Something else? For constructors it's obvious (ish) that the type would be the instance type of the constructor. I'd suggest keeping this request as just about class constructors, so that you don't have to lay out a whole proposal for all possible values.

AFatNiBBa commented 3 weeks ago

The proposal is mainly about class constructors, but I think it could be expanded to every identifier that both stores a value and a type. I wanted to make the proposal as generalised as possible, the specific use case of mixins may not be enough to justify this feature. In that case the value of b would be 1 and the type would be 2

const b = a as type;
// Same as
const b = a;
type b = a;

The main problem would be allowing a function to return a type attached to its return value, but this problem seems to be minimal enough to not evem be mentioned in this reply addressing some of the issues with one of the similiar proposals you provided. (Making the thing opt-in solves those issues by the way)

AFatNiBBa commented 3 weeks ago

(To be clear, the type definition should be completely cloned)

const A = "something";
type A<T> = { idk: T };

const B = A as type;
// Same as
const B = A;
type B<T> = A<T>; // The type parameters are not automatically forwarded
jcalz commented 3 weeks ago

This is a known pain point with class constructors, but not really the kind of thing people have been clamoring for with non-constructor values. The proposal you're talking about doesn't really work for constructors, or at least not consistently. There is no "attached type" for mixin(MyClass), it's just a value. There's no type "named" mixin(MyClass), and the "attached type" is... the instance type? You want const Result = mixin(MyClass) as type to do something fundamentally different from const b = a as type. How do you determine what the "attached type" is for an arbitrary value? What should const b = (1 + 1) as type do? All of this is certainly interesting, but I don't know why you want to water down your request by expanding it well past the use case you care about, to a region where people can talk for pages and pages about what the right thing to do is.

If I were you I would edit this to fix the request so it's not asking for a runtime feature (so class b = a is a no-go, even if we decided what it would mean) and to confine it to just class constructors... and then we can both hide all these comments that merely distract from what I think the main ask is here. It's your choice how to proceed, and I'm not a TS team member, so take or leave this advice as you see fit.

AFatNiBBa commented 3 weeks ago

I updated the main suggestion to change the syntax, provide edge cases and state clearly that the second part of the suggestion is completely optional. I will change the suggestion again as needed as soon as I, eventually, get feedback from TS team members (Potentially removing the optional part altogether)