skip to Main Content

I’m trying to port some JavaScript code to TypeScript.

One of the classes looks like this (playground link):

class Foo {
  constructor() {
    Object.defineProperties(this, {
      a: {
        value: 'a',
      },

      b: {
        value: 'b',
      },

      n: {
        writable: true,
        value: 0,
      },
    });

    assertFooApi(this);
  }

  reset() {
    this.a = 'a';
    this.b = 'b';
    this.n = 0;
  }
}

interface FooApi {
  a: string,
  b: string,
  n: number,
}

function assertFooApi(input: any): asserts input is FooApi {
  // intentionally fake
}

This reset function errors because Object.defineProperties doesn’t affect the type signature in typescript.

I did not write the original JavaScript code nor do I fully understand it. I’m looking for a way to define this‘s type without modifying any behavior. The less changes I make to its existing implementation, the better. Eventually, I’ll write automated tests around it so I can change its implementation, but as a step 1, I think porting it to typescript is the best bang for my buck.

This comment provides two workarounds to the problem. I think they would work if I didn’t need the workaround to apply to this. But as you can see (in the playground link above), using an assert function on this doesn’t modify its internal type.

If a class defines properties on this with Object.defineProperties, how do I use those properties in other internal functions without compilation errors?

3

Answers


  1. Chosen as BEST ANSWER

    I guess this counts as an answer, but I'm hoping there is a better one out there. If you implement reset like this, the errors go away (playground link):

      reset() {
        assertFooApi(this); // this fixes the errors
    
        this.a = 'a';
        this.b = 'b';
        this.n = 0;
      }
    

    Ideally, I wouldn't have to put that at the top of every internal function.


  2. To convert this code to TS without changing its behaviour,

    1. declare class fields (declared fields compile to nothing)
    2. Add readonly for non-writable fields
    3. implement your interface to make sure it’s properly implemented
    class Foo implements FooApi {
      declare readonly a: 'a' // non-writable so readonly literal
      declare readonly b: 'b' // non-writable so readonly literal
      declare n: number // writable
      constructor() {
        Object.defineProperties(this, {
          a: { value: 'a', },
          b: { value: 'b', },
          n: {
            writable: true,
            value: 0,
          },
        });
      }
    }
    

    Remember, that constructor’s return value type doesn’t affect class type in any way. Only fields, methods, and extends clause do.

    Login or Signup to reply.
  3. I’m looking for a way to define this’s type without modifying any behavior

    This is exactly what the declare keyword does. It creates type information but never emits any code.

    It’s rarely used in production code because it’s so easy to make it lie to you about what type something really is. But, for cases like this you can add the properties with declare which tells the type system to expect them to be there.

    IMPORTANT NOTE: Just be careful! If you change the code in defineProperties to create different properties, typescript can’t pair this up and raise type errors. This may lead to runtime crashes. This is not a proper solution. But when porting untyped code to typed code incrementally, sometimes this unsafety is an improvement.

    class Foo {
    
      declare readonly a: string
      declare readonly b: string
      declare n: number
    
      constructor() {
        Object.defineProperties(this, {
          a: {
            value: 'a',
          },
    
          b: {
            value: 'b',
          },
    
          n: {
            writable: true,
            value: 0,
          },
        });
      }
    
      reset() {
        // changed these because they would be readonly
        console.log(this.a); // fine
        console.log(this.b); // fine
    
        this.n = 0; // fine
      }
    }
    

    See Playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search