Open hediet opened 4 years ago
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.props
? Note that this["props"]
is used as type here. You can only use this.props
in a type expression when using typeof
.
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.
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.
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.
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 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"]
overMyComponentProps
. In practice, components get renamed while their props types are forgotten. This cannot happen if you useMyComponent["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.
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 :)
+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
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 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 That is incorrect. It has been possible for a long time to use
this
andthis["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.
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
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
@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.
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.
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 aprivate
constructor and astatic
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.
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.
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.
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?
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).
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> {
...
}
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.
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)
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. Could folks please dial it back a bit so that issues are not judged based on the attitude of their proponents?
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>{}
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. :)
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
@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:
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... :facepalm:
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.
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.
TypeScript developers do not belong to a marginalized group, which makes it challenging to comprehend how civility is being employed as a tool to perpetuate their oppression. Disagreement with the way the TypeScript team maintains this repository does not constitute a wrongdoing that requires discarding politeness to bring about radical change. There is no necessity for one to take any of those actions since TypeScript is not proprietary. The source code is released under the Apache 2.0 License, granting individuals and like-minded developers the freedom to fork the language and shape it in their preferred direction, as well as moderate related forums according to their own judgment. If anyone strongly feels the need to express their disagreement, it is advisable to consider these alternatives or, at the very least, refrain from sending uncivil messages to me in particular. Your consideration is appreciated.
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.
It's clear that we will never agree about this and that further discussion here is only adding noise. I am going to stop commenting about this and hide my comments as off-topic. I will not reply to further comments except as they pertain directly to allowing this
in a constructor parameter.
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>{}
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
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: