-
Notifications
You must be signed in to change notification settings - Fork 12.3k
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
Comments
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". |
@dl748 What problems does this have? What is the correct way to do this? How would you use |
This has a very narrow use case, in that it depends on the calling code (where 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 After the type is named, there are many nifty things we can do with it. As far as |
The use of |
Besides semantical differences between @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 However, I can understand if my arguments for allowing |
This is exactly my use case and I'd love if this was possible in TypeScript :) |
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. |
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 |
+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 class ChildClass<T = ChildClass<any>> extends BaseClass<T> {}
class GrandChildClass extends ChildClass<GrandChildClass> {} This obviously doesn't look great |
Related to #5863? |
@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 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 @dl748 That is incorrect. It has been possible for a long time to use |
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.
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. |
How I would do it
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 |
@dl748 Generics are not the same as As I said, Quite frankly, I have no idea why you are so adamantly against this. It doesn't matter whether you personally think 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
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.
That is because any properties referenced with
And even if somebody used Or maybe they're doing some trickery with
Why do you keep bringing up all these strange things?
There is no variable, |
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"
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.
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.
or 2. if i want to use a variable i have to recreate the interface (arg....)
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
Completely relevant as the example in THIS proposal uses a PRIVATE variable to do the typing. See Above.
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.
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. |
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:
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:
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. |
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 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. |
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 |
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 |
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 abstract class A<T> {
constructor(initializer: Partial<T>) {
...
}
}
class B extends A<B> {
...
} |
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 // 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. |
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 (PS: edited my tone to something more clearly playful) |
This comment was marked as off-topic.
This comment was marked as off-topic.
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
But I have to do messy things like this:
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.
|
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, |
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... 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 |
When someone's tone becomes abrasive, it can mean one of two things:
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:
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:
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 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
Which proves you're not "weird", just experiencing mild gaslighting.
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.
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. |
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 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. |
This comment was marked as off-topic.
This comment was marked as off-topic.
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. |
This comment was marked as off-topic.
This comment was marked as off-topic.
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 |
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); |
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)
: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
Current Workarounds
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:
The text was updated successfully, but these errors were encountered: