Hi all,
we are currently using Ember 3.12 with ember-cli-typescript and would like to upgrade to Ember 3.14+, but as of Ember 3.13 wie experience problems with this upgrade.
In https://github.com/emberjs/ember.js/issues/18614, @pzuraq gave us a very helpful hint that the problem could be ember-cli-typescript or TypeScript itself.
We hope that you can help us.
ember -v hereember-cli: 3.13.2
node: 12.13.1
os: win32 x64
tsc -v hereVersion 3.7.3
ember-cli-typescript and ember-cli-typescript-blueprints here[email protected]
[email protected]
tsconfig.json and tslint.json or eslint.json (if applicable) belowMy tsconfig.json
{
"compilerOptions": {
"target": "es2017",
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noEmitOnError": false,
"noEmit": true,
"inlineSourceMap": true,
"inlineSources": true,
"baseUrl": ".",
"module": "es6",
"experimentalDecorators": true,
"paths": {
"test-model-reactivity/tests/": [
"tests/"
],
"test-model-reactivity/": [
"app/"
],
"": [
"types/"
]
}
},
"include": [
"app//",
"tests//",
"types/*/"
]
}
ember new test-model-reactivity
ember install ember-cli-typescript
ember g route application
ember g controller application
Add the following lines
// routes/application.ts
import Route from "@ember/routing/route";
import RSVP from "rsvp";
export class Example {
constructor(public value: string) { }
}
export class ApplicationModel {
constructor(public example: Example) { }
}
export default class Application extends Route {
model(_params: {}, _transition: any): RSVP.Promise<ApplicationModel> {
return RSVP.resolve(new ApplicationModel(new Example("test")));
}
}
// controllers/application.ts
import Controller from "@ember/controller";
import {ApplicationModel} from "test-model-reactivity/routes/application";
import {action, computed, set} from "@ember/object";
export default class Application extends Controller {
model!: ApplicationModel;
@computed("model.example.value")
get value() {
return this.model.example.value;
}
@action
changeExampleValue() {
set(this.model.example, "value", String(Math.random()));
}
}
// DO NOT DELETE: this is how TypeScript knows how to look up your controllers.
declare module "@ember/controller" {
interface Registry {
"application": Application;
}
}
{{! templates/application.hbs }}
<button {{on "click" this.changeExampleValue}}>
{{this.value}}
</button>
The template should update after the button was clicked.
The template is not updated because the computed property value is not executed.
The JavaScript version of the example above works like expected, only the TypeScript version has this problems.
I've digged into this issue further and i think i've found the root of the problem 😄.
In my reproduction case i've defined a property model: ApplicationModel; in the controller. This overrides the original property model. See the generated JavaScript code (from dist/assets/test-model-reactivity.js):
...
let Application = (_dec = Ember.computed("model.example.value"), (_class = (_temp = class Application extends Ember.Controller {
constructor(...args) {
super(...args);
_defineProperty(this, "model", void 0);
}
get value() {
return this.model.example.value;
}
changeExampleValue() {
Ember.set(this.model.example, "value", String(Math.random()));
}
}, _temp), (_applyDecoratedDescriptor(_class.prototype, "value", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "value"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "changeExampleValue", [Ember._action], Object.getOwnPropertyDescriptor(_class.prototype, "changeExampleValue"), _class.prototype)), _class)); // DO NOT DELETE: this is how TypeScript knows how to look up your controllers.
....
If i comment out the line model!: ApplicationModel; from the controller i've get the following JavaScript output:
...
let Application = (_dec = Ember.computed("model.example.value"), (_class = class Application extends Ember.Controller {
// model!: ApplicationModel;
get value() {
return this.model.example.value;
}
changeExampleValue() {
Ember.set(this.model.example, "value", String(Math.random()));
}
}, (_applyDecoratedDescriptor(_class.prototype, "value", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "value"), _class.prototype), _applyDecoratedDescriptor(_class.prototype, "changeExampleValue", [Ember._action], Object.getOwnPropertyDescriptor(_class.prototype, "changeExampleValue"), _class.prototype)), _class)); // DO NOT DELETE: this is how TypeScript knows how to look up your controllers.
...
So the solution is very simple, remove the line model!: ApplicationModel; and fix the types of @ember/controller to get full type-safety.
// @types/ember__controller
declare module "@ember/controller" {
import ActionHandler from "@ember/object/-private/action-handler";
import Mixin from "@ember/object/mixin";
import EmberObject from "@ember/object";
import ComputedProperty from "@ember/object/computed";
// tslint:disable-next-line strict-export-declare-modifiers
type QueryParamTypes = "boolean" | "number" | "array" | "string";
// tslint:disable-next-line strict-export-declare-modifiers
type QueryParamScopeTypes = "controller" | "model";
/**
* Additional methods for the Controller.
*/
export interface ControllerMixin extends ActionHandler {
replaceRoute(name: string, ...args: any[]): void;
transitionToRoute(name: string, ...args: any[]): void;
model: any;
queryParams: string | string[] | Array<{ [key: string]: {
type?: QueryParamTypes,
scope?: QueryParamScopeTypes,
as?: string
}}>;
target: object;
}
export const ControllerMixin: Mixin<ControllerMixin>;
// tslint:disable-next-line:no-empty-interface
export default class Controller<M = never> extends EmberObject.extend(ControllerMixin) {
model: M;
}
export function inject(): ComputedProperty<Controller>;
export function inject<K extends keyof Registry>(
name: K
): ComputedProperty<Registry[K]>;
export function inject(target: object, propertyKey: string | symbol): void;
// A type registry for Ember `Controller`s. Meant to be declaration-merged
// so string lookups resolve to the correct type.
// tslint:disable-next-line no-empty-interface
export interface Registry {}
}
Then i can rewrite my controller and all works like a charm 😃.
import Controller from "@ember/controller";
import {action, computed, set} from "@ember/object";
import {ApplicationModel} from "test-model-reactivity/routes/application";
export default class Application extends Controller<ApplicationModel> {
@computed("model.example.value")
get value() {
return this.model.example.value;
}
@action
changeExampleValue() {
set(this.model.example, "value", String(Math.random()));
}
}
// DO NOT DELETE: this is how TypeScript knows how to look up your controllers.
declare module "@ember/controller" {
interface Registry {
"application": Application;
}
}
Has anybody a better idea than to fix the types?
Perhaps it would make sense to include this in the documentation to prevent problems when upgrading to Ember 3.13+?
I’ll make sure we cover this failure case more thoroughly in the updated docs, but this is in fact expected behavior (and doesn’t require changing the base types). The problem is what this transpiles to:
class Foo {
bar;
}
The resulting output _per the spec_ is (basically):
class Foo {
constructor() {
defineProperty(this, 'bar', void 0);
}
}
TS has basically "overloaded" the meaning of a property declaration on a class like this, by making it where you declare the type as well. This is what you found in your inspection of the compiled output: even if the declaration is bar: string rather than bar;, it has the same output. The TypeScript team is aware of this issue, and in fact TypeScript 3.7 includes a new declare syntax to support this exact use case. With that new syntax, you'd write it like this:
class MyController extends Controller {
declare model!: ApplicationModel;
// ...
}
I'm not sure what changed between 3.12 and 3.13; it's possible an ordering difference around setupController and the instantiation of the Controller was introduced in certain edge cases. ember-cli-typescript and our compilation pipeline's handling of this specific scenario haven't changed in quite some time, so while it's not impossible that we're doing something wrong, I'd be much more suspicious of changes in the framework.
cc. @rwjblue @pzuraq @chancancode – the only thing I can think of here that would be relevant in the 3.12→3.13 is the prep work that was done to support @model in 3.14?
Hi @chriskrycho, thanks you for your detailed explanation 👍 . I will check the new declare syntax tomorrow.
Hi @chriskrycho, i've tested your proposed solution using "ember-cli-typescript": "3.1.2" with TypeScript "typescript": "3.7.3" but unfortunately this leads to the following error:
...TypeScript 'declare' fields must first be transformed by @babel/plugin-transform-typescript.
If you have already enabled that plugin (or '@babel/preset-typescript'), make sure that it runs before any plugin related to additional class features:
- @babel/plugin-proposal-class-properties
- @babel/plugin-proposal-private-methods
- @babel/plugin-proposal-decorators
...
Do you have any idea whats going wrong?
Yep, that’s a known issue which will have a release with the fix shortly.
The declare issue is being tracked in #1033
You can now use the declare property modifier as of [email protected].