-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Javascript: Object.assign to assign property values for classes is not respected #26792
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
Comments
This comment has been minimized.
This comment has been minimized.
Why not support return different thing from constructor()? class X {
public X: number;
constructor() {
return new Y;
}
}
class Y extends X {
public Y: number;
}
const obj = new X;
console.log(obj.Y); /// TS2339: Property 'Y' does not exist on type 'X' It's more common than only Object.assign. |
Came here from #28883. I understand that in the general case, the type of the second argument to interface MyConfig {
someConfigProp: boolean;
}
class MyClass implements MyConfig {
// Error: Property 'someConfigProp' has no initializer
// and is not definitely assigned in the constructor.
someConfigProp: boolean;
constructor (config: MyConfig) {
Object.assign(this, config);
}
} It seems to me that comparison between the types of The alternative right now is to manually assign every property of the config interface to the corresponding class property, which works but is a pain: interface MyConfig {
someProp: boolean;
someOtherProp: string;
// ...etc
}
class MyClass implements MyConfig {
someProp: boolean;
someOtherProp: string;
// ...etc
constructor (config: MyConfig) {
this.someProp = config.someProp;
this.someOtherProp = config.someOtherProp;
// ...repeated for every property of the config object
}
} (This whole thing gets even more ugly when dealing when using optional properties on the config object to allow the use of default values: interface MyConfig {
// these props are optional in the config object
someProp?: boolean;
someOtherProp?: string;
}
class MyClass implements MyConfig {
// all props are required in the class, with default values
someProp: boolean = false;
someOtherProp: string = 'default value';
constructor (config: MyConfig) {
if (config.someProp !== undefined)
this.someProp = config.someProp;
if (config.someOtherProp !== undefined)
this.someOtherProp = config.someOtherProp;
}
} and as you can see, with lots of properties on the config object, you quickly wind up with a large chunk of code that just does the same job Is there any way to achieve this right now? If not, is it viable to implement? I could look into putting a PR together, but no promises as I've never worked with TS's source before. |
@Geo1088 I think you can just use |
@GongT Thanks for the guidance, don't know why I didn't think to do that myself. |
Not quite, though... The issue was never that the arguments were improperly typed, but that the Maybe a better example: interface MyOptions {
myRequiredProp: string;
}
class MyClass implements MyOptions {
myRequiredProp: string;
constructor (options: MyOptions) {
// The next line guarantees myRequiredProp is assigned, since
// it's required on MyOptions
Object.assign(this, options);
// Next line generates "Property 'myRequiredProp' is used
// before being assigned"
if (this.myRequiredProp === 'some value') {
// additional setup here
}
}
} |
Just stumbled across this issue - I want to assign multiple properties without too much code duplication, but Typescript is complaining when strict flags are set unfortunately. |
Just found this article which outlines a way to accomplish this with the return value of I'm interested in making a PR for this behavior; does anyone more familiar with the project have any pointers on possible starting points? |
Is this a duplicate of #16163? |
Looks like it, sorry for not catching that.
Well this doesn't bode well for my chances making a PR for this, does it... |
You could get around that with a definite assignment assertion ( |
This would be amazing. Right now, if you want to augment a POJO with some methods, say, for computing related values, it's really easy to do in JS — but basically impossible to type in TS without a lot of boilerplate or circular reference issues: class AugmentedPOJO {
a() { return this.b(); },
b() { return this.x; }
constructor(pojo: { x: string }) {
// doesn't work, as pointed out
Object.assign(this, pojo);
}
} If we want to guarantee that the new methods ( function augment(pojo) {
return { ...pojo, ...methods };
}
const methods = {
a() { return this.b(); },
b() { return this.x; }
} But trying to type the The best hack I've come up with is: type Pojo = { x: string };
type AugmentedTrueType = Pojo & _Augmented;
class _Augmented {
constructor(pojo: Pojo) {
object.assign(this, pojo);
}
a(this: AugmentedTrueType) { return this.b(); }
b(this: AugmentedTrueType) { return this.x; }
}
const Augmented = _Augmented as {
new (pojo: Pojo): AugmentedTrueType;
};
// Works as expected!
const a = new Augmented({ x: "hello" }); |
What I ended up doing in my case was to merge/augment the class with an interface like below. This is not a solution to the problem but a workaround. The idea is to have an interface that defines the object passed to the constructor function that has the same name as the class. export interface Awesome {
x: string;
y: number;
z: boolean;
zz?: boolean;
}
export class Awesome {
constructor(props: Awesome) {
Object.assign(this, props);
}
fn() {
return this.x; // TS finds x on this because of the interface above.
}
} If you have a base abstract class that you want to use together with generics to dynamically defines class properties, you can do this: // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/interface-name-prefix
export interface BaseAwesome<T extends IBaseAwesomeProps> extends IBaseAwesomeProps {}
export abstract class BaseAwesome<T extends IBaseAwesomeProps> {
constructor(props: T) {
Object.assign(this, props);
// Some contrived logic to show that both "this" and "props" objects contain the desired object properties.
this.y = props.y > 5 ? 5 : props.y;
}
getX(): T['x'] {
return this.x;
}
updateX(x: T['x']) {
this.x = x;
}
abstract logZZ(): void;
} Then the abstract class can be used like this: export interface IDerivedAwesomeProps extends IBaseAwesomeProps {
someNewProp: 'this' | 'that'; // new prop added on top of base props.
xx: number; // modified base prop to be required and have specific type.
}
export class DerivedAwesome extends BaseAwesome<IDerivedAwesomeProps> {
logZZ() {
console.log(this.zz);
}
}
const awesomeInstance = new DerivedAwesome({
someNewProp: 'that',
x: 'some string value',
xx: -555,
y: 100,
z: true,
});
console.log(awesomeInstance.getX()); // -> some string value
awesomeInstance.logZZ(); // -> undefined |
See #40451 for proposed further improvements and currently possible alternatives |
A workaround solution I used was to create a type alias for the base class. Something like: interface Opts {
a: string;
b: number;
}
export class SomeClass {
constructor(opts: Opts) {
Object.assign(this, opts);
}
}
export type TSomeClass = SomeClass & Partial<Opts>; At least you get intellisense tips and typechecking |
@PLSFIX This helps somewhat for users of the class, but if class methods use any of the properties of If we could have field designators (public/private/protected + readonly) in destructured object parameters this problem would be solved AND we would have the added bonus of being able to initialize parts of an object automatically without having to use
|
I came across a similar issue where I wanted to essentially extend a type from a third-party library, but didn't want to manually write out all of the properties (lazy 😅). This is what I ended up doing: Create class from a generic type: function createClassFromType<T>() {
return class {
constructor(args: T) {
Object.assign(this, args)
}
} as ({
new (args: T): T
})
} Create a base class for my type Person = {
name: string
}
const PersonClass = createClassFromType<Person>()
const person = new PersonClass({ name: 'Alice' })
person.name // Alice Extending class MyPerson extends PersonClass {
constructor(person: Person) {
super(person)
}
greet() {
console.log(`Hello, ${this.name}`)
}
} Here are some more example use cases: const classPerson = new PersonClass({ name: 'Alice' })
console.log(classPerson.name) // Alice
const childPerson = new PersonChild({ name: 'Bob' })
childPerson.greet() // "Hello, Bob!"
function doSomethingWith(personType: Person) {
// example method only acceots person type
}
doSomethingWith(classPerson) // this is ok!
doSomethingWith(childPerson) // this is ok!
if (childPerson instanceof PersonClass) {
console.log('person is instanceof PersonClass!')
}
if (childPerson instanceof PersonChild) {
console.log('person is instanceof MyPerson!')
} |
I am having the exact same problem as @eritbh with Object.assign not making TypeScript recognize the assignment to class properties. Please fix this! |
I've been looking for this for a while and your solution @asleepace is perfectly doing the job! By directly calling the function classFromProps<T>() {
return class {
constructor(props: T) {
Object.assign(this, props);
}
} as ({new (args: T): T});
}
type PersonProps = {
firstname: string,
lastname: string,
};
class Person extends classFromProps<PersonProps>() {
get fullname() {
return [this.firstname, this.lastname].join(' ');
}
}
const ts = new Person({firstname: 'Tom', lastname: 'Smith'});
console.log(ts); // ==> Person: {"firstname": "Tom", "lastname": "Smith"}
console.log(ts.fullname); // ==> "Tom Smith" |
This solution is indeed working, |
Any solution except TypeScript itself handling this will be an ugly workaround. Using a class declaration wrapper function every time we want to declare a class is just hideous. Yes, it works, but so does resorting to |
It's curious why that problem is not solved? while it is not random bug, I can't use Object.assign for assign Mixin's Property at my Class without type error, I think // @ts-ignore is best solution |
@Amiko1 |
Especially because this also affects us vanilla JS users out there who just like Code Lens in VSCode. In normal JS I can't just hack in some interfaces or types, all I got is JSDoc comments and I have no idea how to workaround this with them. |
I am waiting for this feature for years now. I hope it will be solved some day 👍 |
In newer versions of TS you can do the following to let the type system know the properties are not initialized in the constructor: class Person {
public name!: string // <--- notice the !:
public age!: number
constructor(props: { name: string, age: number }) {
Object.assign(this, props)
}
}
https://www.typescriptlang.org/docs/handbook/2/classes.html#--strictpropertyinitialization Looks like you can take this a step further and set Alternatively, you can also use interface Data {
name: string
age: number
}
class Person implements Data {
declare name: string
declare age: number
constructor(props: Data) {
Object.assign(this, props)
}
} Still not great solutions, but they do feel a bit less hacky... |
Probably the assign contract for the export function assign<T extends object, U>(target: T, source: U): asserts target is T & U {
Object.assign(target, source);
} instead of: assign<T extends {}, U>(target: T, source: U): T & U;
|
Perhaps TS should be able to identify
Object.assign(this, ...)
then infer the properties that are assigned tothis
? Or is this not possible to do? I am not sure.TypeScript Version:
Version 3.0.3
Search Terms:
Code
A.js
Expected behavior:
TS should be able to identify
x
as a property ofthis
.Actual behavior:
Throwing:
Playground Link:
http://www.typescriptlang.org/play/#src=export%20default%20class%20A%20%7B%0D%0A%20%20constructor%20(%7B%20x%2C%20y%2C%20z%20%7D)%20%7B%0D%0A%20%20%20%20Object.assign(this%2C%20%7Bx%2C%20y%2C%20z%7D)%3B%0D%0A%20%20%7D%0D%0A%20%20f%20()%20%7B%0D%0A%20%20%20%20return%20this.x%3B%0D%0A%20%20%7D%0D%0A%7D
Related Issues:
no
Side Note:
I am using TS with Javascript because YouCompleteMe switched to using TSServer for semantic completion. I absolutely love TS so was really happy about the switch! Thanks for the great work!
The text was updated successfully, but these errors were encountered: