Skip to content

In JS, there's no way to assert that a property is definitely assigned #23217

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

Open
DanielRosenwasser opened this issue Apr 6, 2018 · 12 comments
Assignees
Labels
Domain: JavaScript The issue relates to JavaScript specifically Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Apr 6, 2018

Keywords: JSDoc, Salsa, JavaScript, definite, initialization, initialized, assigned, assertion

class ElementWrapper {
  constructor() {
    this.init();
  }

  init() {
    this.element = document.createElement('div');
  }

  getElementStyle() {
    this.element.style // error: element is possibly undefined
  }
}

We need a way to convince TypeScript in strictNullChecks that this.element is initialized in getElementStyle. Basically a definite initialization assertion.

Related is #23405 which tracks non-null assertions on the expression level.

@mhegazy mhegazy added the Bug A bug in TypeScript label Apr 6, 2018
@mhegazy mhegazy added this to the TypeScript 2.9 milestone Apr 6, 2018
@mhegazy mhegazy added the Salsa label Apr 6, 2018
@mhegazy
Copy link
Contributor

mhegazy commented Apr 6, 2018

@DanielRosenwasser had an idea of an /** @delayinitialized */ on the declaration..

We can also add a tag on init like /** @initializer */ to treat it like a constructor for purposes of definite assignment detection.

@sandersn sandersn changed the title Definite assignment assertions in JSDoc In JS, there's no way to assert that a property is definitely assigned Apr 6, 2018
@mqudsi
Copy link

mqudsi commented Apr 26, 2018

Wouldn't simply following the constructor call chain be simpler?

@mhegazy mhegazy added Suggestion An idea for TypeScript and removed Bug A bug in TypeScript labels Apr 26, 2018
@mhegazy mhegazy removed this from the TypeScript 2.9 milestone Apr 26, 2018
@RyanCavanaugh RyanCavanaugh added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Aug 22, 2018
@weswigham weswigham added Domain: JavaScript The issue relates to JavaScript specifically and removed Salsa labels Nov 29, 2018
@beauxq
Copy link

beauxq commented Nov 8, 2020

Since someone mentioned following the constructor call chain:
#30462

@mqudsi
Copy link

mqudsi commented Nov 10, 2020

Thanks for the heads-up. I'm always surprised when I remember this isn't the case.

@clshortfuse
Copy link

clshortfuse commented Oct 27, 2022

The only way I can define is in Subclass.prototype after the declaration:

class Superclass extends HTMLElement {
  static {
    console.log(1, 'Super Static Init')
  }

  static superStaticField = console.log(2, 'Super Static Field')

  static observedAttributes = ['foo'];

  constructor() {
    console.log(8, 'Superclass constructor: pre-super()');
    super();
    this.superConstructorVar = console.log(10, 'Superclass constructor: post-super()');
  }

  superInstanceField = console.log(9, 'Superclass Instance Field');

}

Superclass.prototype.superInstanceProp = console.log(3, 'Super post-declaration' );

class Subclass extends Superclass {
  static {
    console.log(4, 'Sub Static Init');
    
    this.prototype.a = 1; // ts-error
    Subclass.prototype.b = 2 // ts-error
  }

  static subStaticField = console.log(5, 'Sub Static Field');

  static get observedAttributes() {
    console.log(6.5, 'HTMLElement reads observedAttributes from subclass');
    return ['foo'];
  }

  attributeChangedCallback() {
    console.log(13, 'Attributes observed');
  }

  constructor() {
    console.log(7, 'Subclass constructor: pre-super()')
    super();
    this.subConstructorVar = console.log(12, 'Subclass constructor: post-super()');
  }

  subclassInstanceField = console.log(11, 'Subclass Instance Field');
}

Subclass.prototype.subInstanceProp = console.log(6, 'Sub post-declaration');


customElements.define('x-sub', Subclass);
console.log('Element added to registry');
const el = document.createElement('x-sub');
console.log('Element created');
el.setAttribute('foo', 'bar');
console.log('Attribute added');

This yields:

1 'Super Static Init'
2 'Super Static Field'
3 'Super post-declaration'
4 'Sub Static Init'
5 'Sub Static Field'
6 'Sub post-declaration'
6.5 'HTMLElement reads observedAttributes from subclass'
Element added to registry
7 'Subclass constructor: pre-super()'
8 'Superclass constructor: pre-super()'
9 'Superclass Instance Field'
10 'Superclass constructor: post-super()'
11 'Subclass Instance Field'
12 'Subclass constructor: post-super()'
Element created
13 'Attributes observed'
Attribute added

I have to use prototype because constructor() and instance class fields are too late. (I would like to append to the shadowroot based on class properties). I've resolved to using post-declaration at the end of the file, but defining properties at the end of the class is awkward. I would rather use the Class static initialization blocks, but Typescript will not define the type there. That's because it rewrites that code as:

_a = Subclass;
(() => {
    console.log(4, 'Sub Static Init');
    _a.prototype.a = 1;
    Subclass.prototype.b = 2;
})();

I'm not sure why the IIFE is needed, but if it can reworked, I think it would be fine.

Playground Link

@ilogico
Copy link

ilogico commented Mar 17, 2023

Following the program flow is not always possible.
Consider this:

class R {
  accept;
  constructor() {
    new Promise(accept => this.accept = accept);
  }
}

@beauxq
Copy link

beauxq commented Mar 17, 2023

Following the program flow is not always possible. Consider this:

class R {
  accept;
  constructor() {
    new Promise(accept => this.accept = accept);
  }
}

It is possible to follow all of the constructor's program flow in that example. And accept is not initialized before the end of that constructor. That is not a relevant example.

@ilogico
Copy link

ilogico commented Mar 17, 2023

@beauxq you're wrong on two points.
It's not possible to follow the program flow because it's impossible to know if the Promise constructor will run the function before it returns, which it does, making you wrong on the second point: accept is initialized before the end of the constructor.
It's fine to be wrong and it's ok to arrogant. Being arrogant and wrong is just hilarious.

@ilogico
Copy link

ilogico commented Mar 17, 2023

When I say it's impossible to know if the Promise constructor runs the executor before returning I mean from the type system's point of view.
We know, of course, it does.

@beauxq
Copy link

beauxq commented Mar 17, 2023

I see.
I don't see why you think it's impossible to know if the Promise constructor will run the function before it returns.

@ilogico
Copy link

ilogico commented Mar 17, 2023

Because the type system has no way of expressing that.

@beauxq
Copy link

beauxq commented Mar 17, 2023

That's just an assertion of what I'm saying I don't see.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: JavaScript The issue relates to JavaScript specifically Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants