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

Allow this in constructor parameter #38038

Open
5 tasks done
hediet opened this issue Apr 18, 2020 · 34 comments
Open
5 tasks done

Allow this in constructor parameter #38038

hediet opened this issue Apr 18, 2020 · 34 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@hediet
Copy link
Member

hediet commented Apr 18, 2020

Search Terms

type, this, constructor, TS2526

Suggestion

Currently, this code fails with A 'this' type is available only in a non-static member of a class or interface.ts(2526):

class MyClass {
    private props: { baz: string };
    constructor(props: this["props"]) {
       this.props = props;
    }
}

It would be awesome if this could be supported in constructor arguments.

Use Cases

This is very useful for react components, as demonstrated in the following example.
More general, sometimes a class extends a generic base class which requires generic data for initialization. If the super class also wants to do some initialization, it needs to pass this generic argument down to its base class. As constructor arguments must be typed, the type of this generic argument must be explicitly stated in the super class.
In those cases, it is very practical to use this to refer to a field of that base class that has the right type.

Examples

class MyComponent extends React.Component<{
    title: string;
}> {
    constructor(props: this["props"]) {
       super(props);
       // ...
    }
}

Current Workarounds

class MyComponent extends React.Component<{
    title: string;
}> {
    constructor(props: MyComponent["props"]) {
       super(props);
       // ...
    }
}

This only works if the base class declares a static props member.
Also, it has the downside that you must write out the name of the current class. This obfuscates the fact that it refers itself.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@hediet hediet closed this as completed Apr 18, 2020
@hediet hediet reopened this Apr 18, 2020
@dl748
Copy link

dl748 commented Apr 18, 2020

This has so many problems. There is already a correct way to do this. And IMO this is very ugly code to look at, at least use "this.props" or "MyComponent.props".

@hediet
Copy link
Member Author

hediet commented Apr 18, 2020

@dl748 What problems does this have? What is the correct way to do this? How would you use this.props? Note that this["props"] is used as type here. You can only use this.props in a type expression when using typeof.

@dl748
Copy link

dl748 commented Apr 18, 2020

This has a very narrow use case, in that it depends on the calling code (where new is called) not caring about types (e.g. like React). It forces any serious calling code to become typeless, which defeats the purpose of using typescript.

The correct way to do this is to use a named type

type PropsType = {
  baz: string
}
// or
interface PropsType {
  baz: string
}

class MyClass {
  private props: PropsType
  constructor(props: PropsType) {
    this.props = props
  }
}

class MyComponent extends React.Component<PropsType> {
  constructor(props: PropsType) {
    super(props)
    // ...
  }
}

This allows for the type to be exported or reused.

class Reuse {
  private t: any; // what type is this?
  public test() {
    const c = new MyClass(t as any); // t would have to be any or typecasted as any to be used
  }
}

The only real way for a developer to use this is to go into the definition file for MyClass and copy and paste the type into the variable they will use. This is far more ugly to use.

And while, yes, you can do constructor(props: { baz: string }) which has the same problem. This is just because it should be allowed, at least as a general rule for function definitions, but it's considered bad design practice to put this on public functions (i.e. library or class).

After the type is named, there are many nifty things we can do with it.

As far as this["props"] vs typeof this.props, at least i feel that the later is far more describing of what is happening. It "looks" cleaner. When looking at this["props"] i need to do 2 tests in my head, is it succeeding a colon, and if so, is this in a object or is it a type definition. You should allow both, if possible.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Apr 20, 2020
@RyanCavanaugh
Copy link
Member

The use of this["props"] over MyClass["props"] seems very marginal - it basically only helps you when there even is a derived class and the derived class doesn't write its own constructor, which doesn't seem terribly likely in practice.

@hediet
Copy link
Member Author

hediet commented Apr 20, 2020

Besides semantical differences between this["props"] and MyClass["props"], I already prefer the use of this over MyClass as it clearly refers to the current class, whereas MyClass only refers to the current class if the current class is named MyClass. The uniform usage of this across multiple classes makes mental patten matching much easier, compared to the use of their individual class names. Also, this is just much more convenient to type and to remember.

@dl748 I usually try to minimize the number of named symbols. It helps auto import and reduces the amount of things you need to remember or rename. This is why I prefer MyComponent["props"] over MyComponentProps. In practice, components get renamed while their props types are forgotten. This cannot happen if you use MyComponent["props"].

However, I can understand if my arguments for allowing this in constructors don't really apply as they favor this over MyClass for the wrong reasons.

@kuba-orlik
Copy link

it basically only helps you when there even is a derived class and the derived class doesn't write its own constructor, which doesn't seem terribly likely in practice.

This is exactly my use case and I'd love if this was possible in TypeScript :)

@dl748
Copy link

dl748 commented Jun 6, 2020

@dl748 I usually try to minimize the number of named symbols. It helps auto import and reduces the amount of things you need to remember or rename. This is why I prefer MyComponent["props"] over MyComponentProps. In practice, components get renamed while their props types are forgotten. This cannot happen if you use MyComponent["props"].

Except this would basically only works with React, its pretty worthless for everyone else. I think a feature that only works with a specific framework is very poor.

This sounds better as a react specific plugin, than should be built into the language.

@kuba-orlik
Copy link

I don't think this use-case is React-specific. In my opinion this use-case is useful whenever creating a framework in general.

I'm transitioning our in-house framework to React and this feature would make the development much easier. We have a class of field types of a collection and each field can take some parameters that customize the behavior. It makes sense to take those parameters in the constructor. But the main abstract Field class does not now yet what shape those parameters need to be. So we have to create a new constructor for each subclass that just calls the super function... That's not pretty :)

@lkster
Copy link

lkster commented Jun 19, 2020

+1

I need to get type of class that inherits some base class there. Like:

class BaseClass {
    public constructor(arg1: typeof this) {}
}

class ChildClass extends BaseClass {}

// so in the end this will work
new ChildClass(ChildClass)

// but this won't as with any other class that extends BaseClass
new ChildClass(BaseClass)

And as I could achieve that with generics like

class BaseClass<T> {
    public constructor(arg1: typeof T) {}
}

class ChildClass extends BaseClass<ChildClass> {}

It makes more problems if I need to extend ChildClass. The solution right now is:

class ChildClass<T = ChildClass<any>> extends BaseClass<T> {}
class GrandChildClass extends ChildClass<GrandChildClass> {}

This obviously doesn't look great

@jcalz
Copy link
Contributor

jcalz commented Jul 8, 2020

Related to #5863?

@Pauan
Copy link

Pauan commented Aug 1, 2020

@RyanCavanaugh I am speaking as a representative of amCharts.

We use this pattern a lot in our code:

export interface IFoo {
    ...
}

export class Foo {
    declare public _properties: IFoo;
 
    constructor(properties: Foo["_properties"]) {
        ...
    }
}

We use this pattern in order to simulate associated types.

We would really like to use this["_properties"], because we often do have classes which inherit from the parent and don't have a constructor of their own. Right now we have to manually create dozens of useless dummy constructors:

export interface IBar extends IFoo {
    ...
}

export class Bar extends Foo {
    declare _properties: IBar;

    constructor(properties: Bar["_properties"]) {
        super(properties);
    }
}

Since all normal methods can use this, why shouldn't constructor also allow it? It's a very bizarre exception that seems to serve no purpose.


@dl748 That is incorrect. It has been possible for a long time to use this and this["foo"] in all normal methods. They are not a new feature, and we use them very extensively in our code (which does not use React). They are incredibly useful and not framework specific at all.

@dl748
Copy link

dl748 commented Aug 1, 2020

@dl748 That is incorrect. It has been possible for a long time to use this and this["foo"] in all normal methods. They are not a new feature, and we use them very extensively in our code (which does not use React). They are incredibly useful and not framework specific at all.

Um no, i just tried it. You CANNOT use the example provided, even if it was a non-constructor. For the exact reasons I specified in my explanation.

  1. Your argument does not match the requested feature
  2. Your argument does not even match the issue I was explaining.

You have an exported type, this makes sense as no one would be able to use the class specified because of it. The main complaint was that they didn't want to create a separate named interface/named typing. Your variable is also public, that allows someone outside of the class to reference the variable and hence the type. This can also work as well.

My complaint is that what is asked for would allow the class to be almost un-construct-able via a variable where you want strict typing.

Even with regular methods, 'this' is just an alias for the current class. I'm completely fine if that's the way we want to go.

If we changed the request to something like, the type must be exportable and this refers to whatever class its defined in. I'm completely fine with it.

I think a better "fix" would be having the compiler check the parent classes to see if that variable is being set to remove the error "has no initializer and is not definitely assigned in the constructor" as its obviously wrong.

On a personal note, I'm not sure I like the fact that typescript does that implicit casting even with normal functions, it can make debugging extremely hard as now you change variables outside of your type scope. The fact that a class can change variables outside of its defined interface is dangerous from a type strict language.

For what you are doing, generics would be a perfect replacement.

@dl748
Copy link

dl748 commented Aug 1, 2020

How I would do it

export interface IFoo {
  bar: string
}

export class Foo<MyType extends IFoo=IFoo> {
  private _properties: MyType;

  constructor(properties: MyType) {
    this._properties = properties;
  }
}
export interface IBar extends IFoo {
  baz: string
}

export class Bar extends Foo<IBar> {
}

Now not only do you not need to REDEFINE _properties, but you don't need a constructor either. And the Bar constructor explicitly needs IBar properties type. And it reduces the number of function/method calls, so it will "run" faster

@Pauan
Copy link

Pauan commented Aug 2, 2020

@dl748 Generics are not the same as this. We had already tried your approach before, it does not work for our use case. Obviously I gave a very simplified example, our real code is much more complicated.

As I said, this has already existed for a long time, it is not a new feature. This issue is not debating how this should behave (since it already behaves as it should), this issue is simply about making the already existing feature work with constructor.

Quite frankly, I have no idea why you are so adamantly against this. It doesn't matter whether you personally think this is useful or not: it is a well established already existing feature which other people do find useful.

Nobody is asking for big changes, nobody is asking for a new feature. We've only ever asked for the already existing feature to work consistently rather than having a weird exception for constructor. That makes the language more consistent and more useful, with no additional cost.

You have an exported type, this makes sense as no one would be able to use the class specified because of it. The main complaint was that they didn't want to create a separate named interface/named typing.

Why do you claim that? Anybody can use the class just fine without needing to reference anything. We have used this pattern for years, we know how it works.

Your variable is also public, that allows someone outside of the class to reference the variable and hence the type.

That is because any properties referenced with this must be public. But that's completely irrelevant to this issue, I don't know why you are bringing it up.

My complaint is that what is asked for would allow the class to be almost un-construct-able via a variable where you want strict typing.

this["foo"] does not prevent strict typing, and it does not make the class unconstructable.

And even if somebody used this in a way which made the class unconstructable, so what? They can simply... choose to not use this in that way.

Or maybe they're doing some trickery with as casting, in which case the class is constructable. This might happen if they use a private constructor and a static method which constructs the class.

If we changed the request to something like, the type must be exportable and this refers to whatever class its defined in. I'm completely fine with it.

Why do you keep bringing up all these strange things? this already has the proper behavior, nobody is asking for its behavior to be changed.

I think a better "fix" would be having the compiler check the parent classes to see if that variable is being set to remove the error "has no initializer and is not definitely assigned in the constructor" as its obviously wrong.

There is no variable, this is a type, it only exists within the compiler. The this type (which exists at compile time) and the this keyword (which exists at runtime) are not the same thing. This issue is solely about the this type.

@dl748
Copy link

dl748 commented Aug 3, 2020

Generics are not the same as this. We had already tried your approach before, it does not work for our use case. Obviously I gave a very simplified example, our real code is much more complicated.

Of course not, and no one is saying it is, but what you are trying to do is some kind of dynamic typing, this is exactly what generics were designed for.

The actual solution to this proposal is to define a named type with inheritance. If you trying to use it get rid of the constructor, inheritance is not discussed in this proposal.

The proposal also further states, "it is very practical to use this to refer to a field of that base class that has the right type", which implies this would NOT be the derived class like how 'this' currently works, but the base class.

I have to wonder, did you just read the title and nothing else, or did you actually agree with the entire proposal as-is? The proposal itself uses the word "generic" so much, that it SCREAMS, just use "generics"

What is being asked for is a way to reuse a variables type in the constructor without having to redefine them. Because someone doesn't want to make an extra define. You are doing that by making interfaces, which is great, promotes re-usability.

But what you asking for is a way for the constructor to take in a dynamic type (by a variable in the class), where the parent class doesn't need to know. This is something generics can do and quite well. If you are having a different problem than whats specified because of generics, then thats another conversation. My example fulfills the issue in your example without having to add this new feature. Please give an example of something this feature would solve, or would require a lot more typescript to "emulate"

Quite frankly, I have no idea why you are so adamantly against this. It doesn't matter whether you personally think this is useful or not: it is a well established already existing feature which other people do find useful.

I'm not against 'this', I'm against this proposal of using it. Write up a new one that is more "correct" and i'll back you. Unfortunately, even correct, this proposal would be on the low back burner, as it doesn't provide anything that can't be done through other means.

You have an exported type, this makes sense as no one would be able to use the class specified because of it. The main complaint was that they didn't want to create a separate named interface/named typing.

Why do you claim that? Anybody can use the class just fine without needing to reference anything. We have used this pattern for years, we know how it works.

While technically true, this would require the user of the class to literally copy code from the library in order to work. The interfaces would have to line up in order to work. If that interface has hundreds of options, that quite painful.

Lets use the example in the proposal, but i'll add a few more variable to show it more

class MyClass {
    private props: {
        var1: string
        var2: string
        var3: string
        var4: string
        var5: string
        var6: string
        var7: string
        var8: string
        var9: string
        var10: string
        var11: string
    };
    constructor(props: this["props"]) {
       this.props = props;
    }
}

In order to call it, i have to do 1 of 2 things.

  1. hard code the object
var x = new MyClass({
  var1:'',
  var2:'',
  var3:'',
  var4:'',
  var5:'',
  var6:'',
  var7:'',
  var8:'',
  var9:'',
  var10:'',
  var11:'',
})

or 2. if i want to use a variable i have to recreate the interface (arg....)

var y: {
        var1: string
        var2: string
        var3: string
        var4: string
        var5: string
        var6: string
        var7: string
        var8: string
        var9: string
        var10: string
        var11: string
} = {
  var1:'',
  var2:'',
  var3:'',
  var4:'',
  var5:'',
  var6:'',
  var7:'',
  var8:'',
  var9:'',
  var10:'',
  var11:'',
}
var x = new MyClass(y)

There is literally no way to get at the type at that point. MyClass["props"] // private variable no touchie

This is the reason that any this['var'], when you do a normal method, REQUIRES it to be public

Your variable is also public, that allows someone outside of the class to reference the variable and hence the type.

That is because any properties referenced with this must be public. But that's completely irrelevant to this issue, I don't know why you are bringing it up.

Completely relevant as the example in THIS proposal uses a PRIVATE variable to do the typing. See Above.

My complaint is that what is asked for would allow the class to be almost un-construct-able via a variable where you want strict typing.

this["foo"] does not prevent strict typing, and it does not make the class unconstructable.

Via a variable, it requires the user of the class to completely reconstruct the interface type, or resort to use 'any' or 'unknown' which breaks typing. Which most users will do because they don't want to completely redefine interfaces.

Or maybe they're doing some trickery with as casting, in which case the class is constructable. This might happen if they use a private constructor and a static method which constructs the class.

If we changed the request to something like, the type must be exportable and this refers to whatever class its defined in. I'm completely fine with it.

Why do you keep bringing up all these strange things? this already has the proper behavior, nobody is asking for its behavior to be changed.

I only made slight suggestions that would enforce USABILITY of the classes

IMHO, this proposal "as-is" is broken, and puts more work on the user of the class, with little benefit, i still agree with @RyanCavanaugh . The dynamic nature of the constructor can be handled by a generic.

I do welcome any updates to this proposal or a new proposal on the matter.

@ghost
Copy link

ghost commented Sep 5, 2020

I have a problem that I think is strongly related to this:

Consider the following tree structure, which is supposed to be regularly subclassed by users:

class TreeNode {
  private _parent: this | null = null;
  public setParent(parent: this) {this._parent = parent;}
}
const myTreeNode = new TreeNode();

Now I want to allow more flexibility by letting a tree node having a parent from a different (sub)class, but with the original behaviour as default, so I try:

class TreeNode<TParent extends TreeNode<any> = this> {
  private _parent: TParent | null = null;
  public setParent(parent: TParent) {this._parent = parent;}
}
const myTreeNode = new TreeNode();

but that gives me the typical error "TS2526: A 'this' type is available only in a non-static member of a class or interface".

So I think this proposal should be accepted because it seems to be the only way for having this as default generic.

@CynicalBusiness
Copy link

I know this issue is over a year old at this point, but it's still open and "awaiting feedback" so I'll throw in my support for this here rather than opening anew (but I'll do so if that's wanted).

I've hit a problem with this very issue a few times now, and here's the latest example, boiled down:

class Parent<T extends Child> {
    // ...
}

abstract class Child {
    public constructor (parent: Parent<this>) { // ts(2526)
        // ...
    }
}

Because I would want my FooChild implementation of Child to only accept Parent<FooChild> (or something assignable to it), but this cannot be done without error. If I set the constructor to be more open (say, Parent<Child>), input of the wrong type could be passed in. This could also be approached with something like abstract class Child<T extends Parent<any> = Parent<this>> as mentioned above but that's also not allowed.

Doing this in method arguments and generics in a class is indeed supported (at least in TS ^4), so it seems odd it's an error in the constructor.

@cyrilluce
Copy link

Use generic for class is ugly, I'm working with redux duck pattern, and need 4 or 5 generic parameters, just like this shit:

interface State {}
enum Types {}
interface Creators {}
interface Selectors {}
interface Options {}

export default class Duck<
  TState = {},
  TTypes = {},
  TCreators = {},
  TSelectors = {},
  TOptions = {}
> extends Base<
  State & Partial<TState>,
  typeof Types & Partial<TTypes>,
  Creators & Partial<TCreators>,
  Selectors & Partial<TSelectors>,
  Options & Partial<TOptions>
> {}

Now I'm using this['xxx'] pattern, It's works perfect!

export type DuckState<T extends BaseDuck> = ReturnType<T["reducer"]>;
class BaseDuck{
  declare State: DuckState<this>
  get selector(): (globalState: any) => this["State"] {
    // ...
  }
  reducer(){
    return null
  }
}

class SubDuck extends BaseDuck{
  reducer(){
    return 'string'
  }
  *test(){
    const state = this.selector(yield select())
    state === 'string'
  }
}

It's just like f(a,b,c,d,e,f) compare to f({a,b,c,d,e,f}), which you choose?

@Jamesernator
Copy link

Jamesernator commented Mar 24, 2022

This is something I've been wanting today to allow for defining circular kinds, this is probably best demonstrated with a (simplified) example. Apologies for the length, but it's hard to demonstrate a motivating example with much less than this.

type SimpleType = "i32" | "i64" | "f32" | "f64" | "externref" | "funcref";
type Type = SimpleType | WasmASTReferenceType;

type Make<Ctx, T> = (ctx: Ctx) => T;

type WastASTReferenceTypeInit = {
    directlyReferencedTypes: ReadonlyArray<WasmASTReferenceType>,
    encodeDescriptor: (index: number) => Uint8Array,
};

// The big goal here is we want to be able to define circular types
class WasmASTReferenceType {
    readonly #directlyReferencedTypes: ReadonlyArray<WasmASTReferenceType>;
    readonly #encodeDescriptor: (idx: number) => Uint8Array;

    // We want "this" type here so that the makeInit is called
    // with the correct subtype
    constructor(makeInit: Make<this, WastASTReferenceTypeInit>) {
        const init = makeInit(this);
        this.#directlyReferencedTypes = init.directlyReferencedTypes;
        this.#encodeDescriptor = encodeDescriptor;
    }
    
    // Gather all (possibly circular) reference types
    #gatherReferenceTypes(
        seen: Set<WasmASTReferenceType>=new Set(),
    ): Set<WasmASTReferenceType> {
        if (seen.has(this)) {
            return seen;
        }
        seen.add(this);
        for (const directRef of this.#directlyReferencedTypes) {
            directRef.#gatherReferenceTypes(seen);
        }
        return seen;
    }
    
    /*
       Some general encoding stuff
    */
    encodeRefTypeTable() {
        const allRefTypes = Array.from(this.#gatherReferenceTypes());
        const encodedDescriptors = allRefTypes.map(([refType, idx]) => {
            return refType.#encodeDescriptor(idx);
        });
        // concat all the encoded sections together
        return new Uint8Array(
            encodedDescriptors.flatMap(encodedDescriptor => [...encodedDescriptor]),
        );
    }
}

class WasmASTStructType<Types extends Record<string, Type>> {
    readonly #types: Types;

    // We still allow further subclassing
    constructor(makeInit: Make<this, Types>) {
        let types!: Types;
        // Note that tupleType having type WasASTTupleTuple<Types> is neccessary
        // here because when we call *our own* makeInit it must be of "this" type
        super((tupleType) => {
            // NOTE: We can't actually call with "this" here because "this"
            // has not been set yet in our current context, hence tupleType
            // needs to be the correct type, while it is technically a partially
            // constructed instance, it is neccessary for defining circular types
            // SEE example below
            ({ types } = makeInit(tupleType));

            return {
                directlyReferencedTypes: Object.values(types)
                    .filter(type => type instanceof WasmASTReferenceType),
                encodeDescriptor: (index) => {
                    return Uint8Array.from([
                        0x78, // some magic byte denoting the kind
                        index, // in practice this would be encoded as LEB128, but whatever
                    ]);
                },
            };
        });
        
        this.#types = types;
    }
    
    get types(): Desc {
        return this.#desc;
    }
}

// SUPPOSE there is also a WasmASTNullableType defined similarly
declare class WasmASTNullableType extends WasmASTReferenceType {
    constructor(makeType: Make<this, WasmASTReferenceType>);
}

// And now, an example of a circular type

const linkedListOfI32: WasmASTStructType<{
    value: "i32",
    next: WasmASTNullableType<typeof linkedList
}> = new WasmASTStructType(
    // Note that linkedListOfI32 NEEDS TO BE the same type as typeof linkedListOfI32
    (linkedListOfI32) => {
        return {
            value: "i32",
            // Note that linkedListOfI32 needs to be "typeof linkedListOfI32" here
            // otherwise this property is a type error
            next: new WasmASTNullableType(linkedListOfI32),
        }
    },
);

Now the status quo isn't the absolute worst here, we just need a bunch of casting at every usage:

class WasmASTStructType<Types extends Record<string, Type>> {
    readonly #types: Types;

    // We declare the arg explictly
    constructor(makeInit: Make<WasmASTStructType<Types>, Types>) {
        let types!: Types;
        super((tupleType) => {
            // And CAST into "this" type here
            ({ types } = makeInit(tupleType as this));

But of course as with any type assertions we lose type safety, also arguably tupleType having type WasmASTStructType<...> is actually more correct here than WasmASTReferenceType<...>, as while fields are not installed, the prototype is actually correct (i.e. this in makeInit DOES have WasmASTStructType.prototype as it's prototype, so methods are actually available, just not fields).

@soffyo
Copy link

soffyo commented Sep 3, 2022

I want to give a +1 describing the use case that bought me here. Consider an abstract class with a constructor that adds properties to it

abstract class A {
    constructor(initializer: Partial<this>) { // <-- using this type in a constructor is not allowed!
        for (const [key,value] of Object.entries(initializer)) {
            Object.defineProperty(this, key, { value, enumerable: true })
        }
    }
    static someStaticMethod() {}
    someInstancemethod() {}
}

This class will be extended by other classes, declaring their own type

class B extends A {
    a?: string
    b?: number
    c?: boolean
}

When these children classes are constructed, they should only declare own properties

new B({ a: "hello", b: 1, c: true, d: "this must error" }) // <-- We want only properties of B here

Actually, the only possible way of achieving this (using the new syntax when constructing the class) is adding a type parameter to class A and clumsily repeating class B extends A<B>

abstract class A<T> {
    constructor(initializer: Partial<T>) {
        ...
    }
}

class B extends A<B> {
    ...
}

@egasimus
Copy link

egasimus commented Oct 4, 2022

It's things like this that made me get my soapbox.

Hooray for TypeScript! Making every nice thing about JavaScript as bad as the worst things about JavaScript, for no reason, since whenever.

Let's consider the simplest case, a value object taking only those values from the options object that are defined on the class:

// javascript
class ValueObject {
  constructor (options) {
    for (const [key, val] of Object.entries(options)) {
      if (key in this && typeof this[key] !== 'function' ) this[key] = val
    }
  }
}

It hurts me to have to explain this to the TypeScript devs (whom I expect to be much more advanced programmers than myself), but one of the nice things is being able to pass an options object to a constructor. It's nice because it's very simple and very powerful. That's why it's very common, because it gives you a primitive for elaborating on definitions. I notice the TypeScript compiler codebase not doing that a whole lot - instead it uses large numbers of positional arguments. Well, whatever.

You use it like this:

// still javascript
class Bird extends ValueObject {
  family = undefined
}
class Duck extends Bird {
  family = 'Anas'
  species = undefined
  quack () { console.log('Quacking as', this.family, this.species) }
}
const bird1 = new Bird({ family: 'Passer', species: 'griseus' })     // a sparrow
const bird2 = new Duck({ species: 'platyrhynchos' })                 // a regular duck
const bird3 = new Duck({ family: 'Melanitta', species: 'deglandi' }) // a special duck

const bird4 = new Duck({ arms: 4 }) // an impossible duck

Defining the arms of a Bird is a nonsensical operation which you want the type checker to warn you about ASAP, right? So let's see how to add types to the pattern:

// suddenly, typescript:
class ValueObject {
  constructor (options: Partial<this> = {}) { // TS2526 that's all, folks!

And here am I expecting TypeScript's type inference to cover this case and give me automatic well-typed constructors all the way down the inheritance chain, like a complete and utter... rational person?

You can't seriously pretend that TypeScript is a "superset" of anything at all if it makes extremely common patterns like these so inconvenient to use.

I jumped on the TS wagon late enough, but if issues like this persist I don't want to imagine what it was in the early days - and how much it must've cost Microsoft's to shill this vicious technical debt trap far and wide.

@Helveg
Copy link

Helveg commented Jan 15, 2023

I second that the options object in the constructor is a common enough pattern to be supported in TypeScript. I forgot my soapbox in the Python bug tracker, so I'll keep it short with an example:

class BaseClass<T extends BaseClass<T>> {
    constructor(props: Partial<T>) {
        Object.assign(this, props);
    }
}

class User extends BaseClass<User> {
    name?: string;
    age?: number;
}

const user = new User({ name: "John" });

That's the best we can currently do without this in the constructor. Now - we the propopents - are clearly little rascals, because we demand User extends BaseClass <User> 😇 See this SO question

(PS: edited my tone to something more clearly playful)

@jcalz

This comment was marked as off-topic.

@nhusby
Copy link

nhusby commented Mar 2, 2023

This (pun slightly intended) would be super useful. I'm trying to enforce typing to protect my project. Maybe I'm weird, but I have all kinds of use cases where I define an abstract base class that accepts a subset of the extended classes in the constructor.

Ideally it would look like this

abstract class Base {
  constructor( data: Partial<this> ){}
}

class ChildOfBase extends Base {}

But I have to do messy things like this:

   abstract class Base<T>{
     constructor( data: Partial<T> ){}
   }
   
   class ChildOfBase extends Base<ChildOfBase> {}

This is repetitive and invites incorrect implementation when new devs that don't understand the pattern come along and add more child classes. Preventing this kind of thing is the reason I use TypeScript. To protect us from ourselves.

I expect to see lots of this kind of thing when I use this pattern because no one is going to understand why it has to be the way it does.

AnotherChildOfBase extends Base<any>{}

@LaysDragon
Copy link

LaysDragon commented Apr 3, 2023

Bump into this too, simply I need to declare a series of data class that accept their member field as params,and I hope to use a base class to make things more easier and still type-safety, this type is inaccessible in constructor parameters is a bit surprising. I know generic can handled, but this can be more convenience with the existed feature,and still force the type safety for this kind of scenario. Well I agree its a kind of marginal situation but its still have its usage in marginal practice not complete useless ,or there won't be more people coming here because of people still bumping into this abandoned little corner. :)

@frank-weindel
Copy link

frank-weindel commented Jul 10, 2023

Throwing this out there since it's been awhile. I'd argue that sometimes we'd like to avoid generics in a class hierarchy. The use of generics arguments implies that we'd like to enable class to be instantiated on its own with any types. That's fine for something like a List... new List<number>(), new List<string>().

Sometimes you don't even want that option to be present to a user of your framework. Instead you want to require them to create a subclass and explicitly declare extended types for member properties of the base class. This by the way, makes it much easier to support an infinite hierarchy of extended classes. With generics, one needs to make sure that each level of their class hierarchy has the appropriate generic arguments and constraints in order to enable the next level of extension. If there are many generic arguments, this becomes very tedious and potentially error prone... What's almost worse is that it can clutter contextual hover hints significantly, since those generic parameters are often printed out even if only the default ones are used.

A simple example, similar to some above, that would benefit from supporting this feature: Playground Link

@egasimus
Copy link

egasimus commented Jul 10, 2023

@jcalz

I would like to see this supported too, but I'm concerned that this issue will be locked due to violating the code of conduct, as has happened before when people's tone became too abrasive.

When someone's tone becomes abrasive, it can mean one of two things:

  • Either, they are acting in bad faith and trying to get what they are not owed. In this case, good faith actors should defend themselves.
  • Or, they are acting in good faith but do not feel as if they are being heard. In this case, good faith actors should demonstrate empathy and kindness, especially if they are in a position of power compared to the person whose tone is abrasive.

It's one or the other, and (until someone escalates the violence, turning the situation into an incomprehensible mess) it tends to be quite clear which one it is, should all parties be willing to consider the facts of the matter.

The facts of the matter are that that TypeScript, once again, blocks your path. A random barrier, in the way of a completely valid pattern, for no reason, and the developers impacted by this are being effectively stonewalled when they voice their complaints.

In many open source projects, the maintainers, driven by empathy and kindness, proactively explain the technical reasons and implementation details that make things like TS2526 necessary. Not write off valid use cases as "marginal", or only ever provide an in-depth technical explanation of a persistent issue when they want people to STFU.

You suggest that people frustrated by the current state of affairs should change their tone in accordance with the code of conduct, as if that will make it easier for them to be heard. A more literal reading of what you propose is:

"The TypeScript team is working hard so that you can have these problems. Be nice to them or you will make things worse: they will not only keep ignoring your struggles, but also terminate the conversation with extreme prejudice."

Therefore, I believe you have no right to suggest that they modify their tone, and in this situation you are acting as an enabler of the systemic abuse. Let's look at the code of conduct:

Examples of behavior that contributes to a positive environment for our community include:

  • Demonstrating empathy and kindness toward other people
  • Being respectful of differing opinions, viewpoints, and experiences
  • Giving and gracefully accepting constructive feedback
  • Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
  • Focusing on what is best not just for us as individuals, but for the overall community

Well, I observe the people responsible for TypeScript actively abstaining from all of the above. Now, where is the enforcement mechanism that I can use to get them back in line? Oh wait, there isn't one; trying to make the one described in the CoC work both ways will only be misconstrued as Private or public harassment. On the other hand... 🤦

What we see above is a lot of well-intentioned, professional people tone-policing themselves, to avoid their perfectly valid concerns hypothetically being misread as Disruptive behavior:

@nhusby:

Maybe I'm weird, but I have all kinds of use cases

Which proves you're not "weird", just experiencing mild gaslighting.

@LaysDragon:

Well I agree its a kind of marginal situation but its still have its usage in marginal practice not complete useless ,or there won't be more people coming here because of people still bumping into this abandoned little corner. :)

Using the full capabilities of JavaScript, as specified by ECMA-262 and implemented by your runtime of choice, is not a marginal situation.

I'm starting to think that gaslighting in the TypeScript ecosystem is far from marginal, too.

@Helveg:

(PS: edited my tone to something more clearly playful)

PSA, folks: Your concerns are valid; so is your frustration. You owe it to nobody to be "playful", or tone-police yourself all the way down to an "abandoned little corner" when you're fighting back against Microsoft trying to force artificial obsolescense upon your skillset by normalizing the replacement of standard ECMA-262 with a proprietary dialect whose development is arbitrarily opaque to your feedback.

Talking in this self-deprecatory manner is not "professional behavior"; forcing someone to talk this baby talk is downright cruel. Even toxic 90s hacker flamewar culture is more inclusive than this infantilizing and manipulative environment. Dear developers, this sort of tomfoolery is actively harmful for you. You have the option, and the right, to not concede to it -- as per the code of conduct, in addition to the mandates of basic human decency.

@matthewvalentine
Copy link

Just a slightly different usecase than provided so far. Imagine a class that emits events related to itself. The goal is that you can register for events on some subclass of Foo and still be assured of what, exactly, you're going to get.

class Foo {
   onEvent?: (v: this) => void;
   foo() { this.onEvent?.(this); }
}

class Bar extends Foo {
    extraSpecialMethod() {}
}

const b = new Bar();
b.onEvent = (target) => target.extraSpecialMethod();

That works fine, but the onEvent handler can be left unset. So there's value in doing it in the constructor:

class Foo {
    constructor(private onEvent: (v: this) => void) {}
    foo() { this.onEvent(this); }
}

I think you could argue about whether this is a good pattern, but I don't think whether it's a constructor parameter is relevant to any of that discussion, and that's the only part that matters in regards to this issue. TS is happy to allow the non-constructor version and the two essentially work the same way.

@jcalz

This comment was marked as off-topic.

@egasimus
Copy link

egasimus commented Jul 12, 2023

It's quite prejudiced to imply that marginalized groups are the only entities in the world that are susceptible to systemic abuse. Furthermore, considering one's refusal to adopt a self-deprecatory tone as a violation of civility is an example of gross cultural insensitivity. In my culture, appealing to supposed violations of unwritten social norms for the express purpose of ignoring the actual concerns expressed in an act of communication, is considered hypocrisy or worse. Pointing out where the operating standard of "civility" is unambiguously defined, and consequently where it is being violated, would be a fair start to an actual discussion (that of course few of those whose livelihood depends it on it would be comfortable with initiating.)

Bearing in mind the power differential between a transnational megacorporation and the diverse landscape of disparate individuals who ultimately sustain its products by devoting time and attention, any references to the theoretical openness of TypeScript and to "civility" can only serve as a politically expedient form of "my way or the highway". That nobody is held against their will in the TypeScript ecosystem (which is itself debatable) is not relevant to objective deficiencies such as the issue reported in this thread, nor to the TypeScript team's choice to act if it's a non-issue.

Counterexamples to my main point (that TypeScript maintainership exhibits a pattern of arbitrarily deprioritizing issues that have to do with JavaScript backwards compatibility, while simultaneously adhering to the factually incorrect talking point that "TypeScript is a superset of JavaScript", and relying on nonsensical arguments about the manner in which that concern is expressed, in order to downplay the difference and shut down people who highlight the impact of said issues), would not only be highly appreciated, but actually matter quite a bit more than if people just did what is suggested and refrained from adopting an attitude of seriousness in the face of matters which may directly impact their livelihoods.

@jcalz

This comment was marked as off-topic.

@jonlepage
Copy link

This (pun slightly intended) would be super useful. I'm trying to enforce typing to protect my project. Maybe I'm weird, but I have all kinds of use cases where I define an abstract base class that accepts a subset of the extended classes in the constructor.

Ideally it would look like this

abstract class Base {
  constructor( data: Partial<this> ){}
}

class ChildOfBase extends Base {}

But I have to do messy things like this:

   abstract class Base<T>{
     constructor( data: Partial<T> ){}
   }
   
   class ChildOfBase extends Base<ChildOfBase> {}

This is repetitive and invites incorrect implementation when new devs that don't understand the pattern come along and add more child classes. Preventing this kind of thing is the reason I use TypeScript. To protect us from ourselves.

I expect to see lots of this kind of thing when I use this pattern because no one is going to understand why it has to be the way it does.

AnotherChildOfBase extends Base<any>{}

it exactly my usercaze here !
i want avoid declare constructor or avoid passe the self class type in the generic !!
image

image

@AFatNiBBa
Copy link

I'm simplifying a lot, but I need this to basically store into an object the list that contains it

abstract class Something {
    listOfThis: this[];

    //                      ↓ Error
    constructor(listOfThis: this[]) {
        this.listOfThis = listOfThis;
    }
}

class Derived extends Something { }

const list: Derived[] = [];
list.push(new Derived(list));

I need this because Something NEEDS to be derived a lot, and each derived instance NEEDS to have access to a list that contains ONLY instances of the same derived type

@jahudka
Copy link

jahudka commented Jul 7, 2024

another simple and imo perfectly valid use-case:

export class Metadata {
    constructor(readonly parent?: this) {
    }
}

export class ContainerMetadata extends Metadata {
    private readonly properties: string[] = [];

    addProperty(property: string): void {
        this.properties.push(property);
    }

    getProperties(): string[] {
        // this is the bit that won't work if `this.parent` is e.g. just `Metadata`:
        return this.parent ? [...this.parent.getProperties(), ...this.properties] : this.properties;
    }
}

const parent = new ContainerMetadata();
const child = new ContainerMetadata(parent);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests