Typescript: Javascript: Object.assign to assign property values for classes is not respected

Created on 30 Aug 2018  路  14Comments  路  Source: microsoft/TypeScript

Perhaps TS should be able to identify Object.assign(this, ...) then infer the properties that are assigned to this? Or is this not possible to do? I am not sure.


TypeScript Version:
Version 3.0.3


Search Terms:

  • object assign
  • class
  • does not exist on type

Code
A.js

export default class A {
  constructor ({ x, y, z }) {
    Object.assign(this, {x, y, z});
  }
  f () {
    return this.x;
  }
}

Expected behavior:
TS should be able to identify x as a property of this.

Actual behavior:
Throwing:

Property 'x' does not exist on type 'A'.

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!

Experience Enhancement Suggestion

Most helpful comment

Came here from #28883. I understand that in the general case, the type of the second argument to Object.assign could be of type any, but in my specific case I define that type via an interface beforehand:

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 this and config could be done, and if config is some type that's not assignable to this, then that would be the condition where you couldn't determine the type.

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 Object.assign normally does, as it automatically handles optional properties. However, because Object.assign interprets a key with value undefined as present, this would be blocked on #13195 for full compatibility.)

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.

All 14 comments

Actually, it might not be possible to support this feature for ambiguous objects.

E.g.

Object.assign(this, obj1);

Since obviously obj1 can be of any type in JS; hence there is no way to find out what is being assigned to this.

But I think it would be a good idea to be able to support explicit assignment to this with Object.assign.

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 Object.assign could be of type any, but in my specific case I define that type via an interface beforehand:

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 this and config could be done, and if config is some type that's not assignable to this, then that would be the condition where you couldn't determine the type.

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 Object.assign normally does, as it automatically handles optional properties. However, because Object.assign interprets a key with value undefined as present, this would be blocked on #13195 for full compatibility.)

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 Object.assign(this, config as any), It's really safe in your case

@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 Object.assign call doesn't update which properties are present on its first argument, in this case the constructed instance.

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 Object.assign(), but doesn't address performing the in-place operation/modifying the type of the first argument without reassignment. https://spin.atomicobject.com/2018/05/14/type-safe-object-merging-2-8/

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.

I don't know if that is even something you can even express with the current type system

Well this doesn't bode well for my chances making a PR for this, does it...

Next line generates "Property 'myRequiredProp' is used before being assigned"

You could get around that with a definite assignment assertion (!), but it would be nice if TypeScript could figure that out instead.

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 鈥斅燽ut 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 (a and b) won't be shadowed by an own prop of the pojo, we could do something in vanilla JS like:

function augment(pojo) {
  return { ...pojo, ...methods };
}

const methods = { 
  a() { return this.b(); },
  b() { return this.x; }
}

But trying to type the this for the functions on the methods objects (which should be { x: string } & typeof methods) blows up with circularity errors that are only avoided when using a class.

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 https://github.com/microsoft/TypeScript/issues/40451 for proposed further improvements and currently possible alternatives

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  路  3Comments

blendsdk picture blendsdk  路  3Comments

wmaurer picture wmaurer  路  3Comments

Zlatkovsky picture Zlatkovsky  路  3Comments

dlaberge picture dlaberge  路  3Comments