How to declare optional instance property to EmberObject in ES6 class in Typescript?


#1

Situation: An EmberObject declared as an ES6 class has an instance property. The create({…}) may or may not supply a value. If the property doesn’t have a value upon construction, the constructor will give it a value. I am using strict TypeScript.

  • If I use item : ItemType, the if (!this.item) in the constructor will get a type error.
  • If I use item?: ItemType, all my methods will require guard clauses when using it.
  • If I use item! : itemType, I will get no errors, but nothing will verify that the constructed property has a value. (My declaration merely asserts that - trust me - it does. If I trusted me, I wouldn’t be using TypeScript. :slight_smile: )

If I use a POJO built using a “real constructor” rather than an EmberObject with create({}), does TypeScript offer a better way to express this intention?

Or is the cleanest option to use two values, an itemParm?: ItemType for the external input, and an item: ItemType that is initialized in the constructor and used throughout the rest of the class? The problem here is that the naming is guaranteed to be terrible.

Is there anything that can be done with decorators to get the desired effect?


#2

Short answer: In this case, you should go ahead and do item!: itemType, since you can be sure that either the constructor or the .create() invocation will give it a value, and the ! annotation is designed to tell the compiler precisely that. Unfortunately, the intersection of regular constructors and .create() semantics basically forces our hand here.

If I use a POJO built using a “real constructor” rather than an EmberObject with create({}), does TypeScript offer a better way to express this intention?

In the case of a POJO with a constructor, you can just use default values on the constructor (even with an options hash), which you then assign inline as usual.

class Example {
  item: string;

  constructor({ item = 'a default value' } = {}) {
    this.item = item;
 }
}

let a = new Example();
console.log(a.item);  // "a default value"

let b = new Example({ item: 'something different' });
console.log(b.item); // "something different"

The pattern looks pretty weird, but it does exactly what you want. It’s combining two different kinds of default assignment and restructuring, which is why it looks weird, but it has roughly the same behavior as the .create() way of doing things does.

Or is the cleanest option to use two values, an itemParm?: ItemType for the external input, and an item: ItemType that is initialized in the constructor and used throughout the rest of the class? The problem here is that the naming is guaranteed to be terrible.

In a Glimmer Component future (and indeed in GlimmerJS today), we’ll actually get this for components for free, which I’m looking forward to. You’ll have an immutable args.item which you can then use to assign local values in the constructor.

Is there anything that can be done with decorators to get the desired effect?

Unfortunately, no. At least at this point, TypeScript’s position on decorators is that they cannot affect the type of the thing they decorate in any way. This means that, for example, when you inject a service using @service, you have to apply the ! annotation there as well:

import Component from '@ember/component';
import { service } from '@ember-decorators/service';

import Session from 'my-app/services/session';

export default class MyComponent {
  @service session!: Session;
}

It’s conceivable that if decorators actually (finally!) advance to Stage 3 in the TC39 process come May, that that TS team will revisit that—I really hope they do, because it means that in a scenario like this, we could use the @argument decorator and not have to apply the ! to it—but I have no idea whatsoever of the likelihood of that!