Skip to content
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

Typescript #1106

Closed
wants to merge 17 commits into from
Closed

Typescript #1106

wants to merge 17 commits into from

Conversation

kturney
Copy link
Collaborator

@kturney kturney commented Jan 16, 2020

This PR converts the js in the addon folder to typescript.

Sorry for submitting this PR without discussion first.
It started out as an "I wonder if I can..." after reading #733 (comment).
I'm also submitting it in the spirit of typed-ember/ember-cli-typescript#48.

If we like this, I can also set about converting the addon-test-support code.

@kturney
Copy link
Collaborator Author

kturney commented Jan 16, 2020

It seems tests are failing because travis is using node 8 and a dep wants node 10.
I can update the node version for travis in this PR, if desired.

@jasonmit
Copy link
Member

It seems tests are failing because travis is using node 8 and a dep wants node 10.

I'd like to drop node 8 as part of 5.0.0-stable so feel free to bump in the Travis config and make the commit with BREAKING.

As for the rest of the PR, I'll need some time to go over it. Thanks a ton for taking this on!

export class TranslationModel extends EmberObject {
translations: Map<string, string> = new Map();

init() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be done in the constructor?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's my understanding that the declaration (translations: Map<string, string> = new Map();) should have initialized translations in the constructor. For some reason in older ember versions that was not the case, so I initialize in init if it's missing. I can investigate wether manually initializing in the constructor instead of init works in the older versions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ember-lts-2.16 and ember-lts-2.18 scenarios fail when initializing in the constructor instead of init.

/**
* Adds a translation hash
*/
addTranslations(translations: any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think should be Map<string, string>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this translations param is a JSON blob, I believe, but maybe something like the following would suffice?

interface Translations {
  [key: string]: Translations | string
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a JSON blob, you probably want it to be [key: string]: Translations | string | undefined.

Given how common this particular kind of thing is, you may want to define a Dict type like this:

interface Dict<T> {
  [key: string]: T | undefined;
}

Then in scenarios like this you'd type it as:

export class TranslationModel extends EmberObject {
  // ...
  addTranslations(translations: Dict<Translations | string>) {


set(this: OverridableProps & IntlService, _, localeName: LocaleName) {
// console.log('set locale', localeName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

if (!isArrayEqual(proposed, this._locale)) {
this._locale = proposed;
cancel(this._timer);
this._timer = next(() => this.trigger('localeChanged'));
if (this._timer) cancel(this._timer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

if (this._timer) {
  cancel(this._timer);
}


/** @public **/
addTranslations(localeName, payload) {
addTranslations(localeName: string, payload: any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map<string, string>

export default class FormatterBase {
get options() {
return emberArray();
export interface FormatterOptions {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be typed per each formatter since they have slightly different shapes/options

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. Will do.

format(value: any, options?: object): FormatResult;
}

export default interface Formatter<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you fix the above, this would become Formatter<T, FormatterOptionsType>

@@ -0,0 +1,15 @@
import { assert } from '@ember/debug';

function assertIsString(val: any): asserts val is string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asserts val is string - Is this a typescript feature? If so, pretty awesome!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonmit
Copy link
Member

Took a first pass at it, looks great so far! Left some comments.

Copy link
Collaborator Author

@kturney kturney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick feedback!
I'll get these fixed up tomorrow.

/**
* Adds a translation hash
*/
addTranslations(translations: any) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this translations param is a JSON blob, I believe, but maybe something like the following would suffice?

interface Translations {
  [key: string]: Translations | string
}

export class TranslationModel extends EmberObject {
translations: Map<string, string> = new Map();

init() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's my understanding that the declaration (translations: Map<string, string> = new Map();) should have initialized translations in the constructor. For some reason in older ember versions that was not the case, so I initialize in init if it's missing. I can investigate wether manually initializing in the constructor instead of init works in the older versions.

export default class FormatterBase {
get options() {
return emberArray();
export interface FormatterOptions {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. Will do.

@@ -0,0 +1,15 @@
import { assert } from '@ember/debug';

function assertIsString(val: any): asserts val is string {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kturney
Copy link
Collaborator Author

kturney commented Jan 29, 2020

I rebased to get travis running again.
https://travis-ci.community/t/travis-ci-stopped-tracking-building-a-pr/4159/3

@jasonmit
Copy link
Member

CI should now be fixed if you rebase

@fabmiz
Copy link

fabmiz commented Feb 21, 2020

@kturney; @buschtoens are there reasons this is not getting your thumbs up? I could help if need be.

@buschtoens
Copy link
Member

Time only, I guess. 😅

I've moved this to my weekend to-do list now. I hope I get some time for it. Thank you for pinging!

Copy link
Member

@buschtoens buschtoens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great already! Thanks for this awesome PR. 🎉

I only got to review a first chunk so far and will try to follow-up with the rest ASAP.

Comment on lines +69 to +76
// all TypeScript files
{
files: ['**/*.ts'],
rules: {
// These are covered by tsc
'no-undef': 'off',
'no-unused-vars': 'off'
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no critique against this PR in any way and more of a note to myself and other maintainers: I personally have a great distaste for these overrides-based eslint configs.

At our company we use multiple root eslint configs per package with great success. This works especially well, when you have browser TypeScript and Node JavaScript files in the same pacakge.

We might want to use a setup like the one suggested in @clark/eslint-config-ember in the future.

Comment on lines 25 to 27
export default function makeIntlHelper<T, A1, A2>(
fn: (intl: IntlService, arg1: A1, arg2: A2) => T
): (arg1: A1, arg2: A2) => T;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work?

export default function makeIntlHelper<T, A extends unknown[]>(
  fn: (intl: IntlService, ...args: A) => T
): (...args: A) => T;

Comment on lines +11 to +17
export default function pickLastLocale(locale: string | string[]) {
if (Array.isArray(locale)) {
return locale[locale.length - 1];
}

return locale;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some might say an arrow function would be easier to read:

export default (locale: string | string[]) =>
  Array.isArray(locale) ? locale[locale.length - 1] : locale;

But fine with me. 👍

@@ -9,7 +9,7 @@ import omit from 'lodash.omit';
* @param {object} obj
* @return {string}
*/
const stringifyDeterministically = obj => JSON.stringify(obj, Object.keys(obj).sort());
const stringifyDeterministically = (obj: Record<string, any>) => JSON.stringify(obj, Object.keys(obj).sort());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const stringifyDeterministically = (obj: Record<string, any>) => JSON.stringify(obj, Object.keys(obj).sort());
const stringifyDeterministically = (obj: Record<string, unknown>) => JSON.stringify(obj, Object.keys(obj).sort());

@@ -20,7 +20,7 @@ const stringifyDeterministically = obj => JSON.stringify(obj, Object.keys(obj).s
* @param {string} subject
* @return {string}
*/
const replaceInterpolators = subject =>
const replaceInterpolators = (subject: any) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const replaceInterpolators = (subject: any) =>
const replaceInterpolators = (subject: unknown) =>

@@ -68,4 +69,5 @@ export const serializeTranslation = (key, options) => `t:${key}:${stringifyOptio
* @return {string}
* @hide
*/
export const missingMessage = (key, locales, options) => serializeTranslation(key, options);
export const missingMessage = (key: string, _: string[], options?: Record<string, any>) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const missingMessage = (key: string, _: string[], options?: Record<string, any>) =>
export const missingMessage = (key: string, _locales: string[], options?: Record<string, unknown>) =>

Comment on lines 6 to 9
interface Hooks {
beforeEach: (fn: Function) => any;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ember-qunit already exposes NestedHooks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ember-qunit defines NestedHooks, but from what I could tell it isn't exported

Comment on lines 10 to 13
interface ApplicationContext {
owner: any;
intl?: IntlService;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { TestContext } from "ember-test-helpers";

export interface IntlTestContext extends TestContext {
  intl: IntlService;
}

This would allow consumers to do this in their tests:

import { setupIntl, IntlTestContext } from 'ember-intl/test-support';

module('foo', function() {
  setupIntl(this);

  test('foo', (this: IntlTestContext, assert) {
    this.intl.setLocale('en-US');
    // ...
  })
});

Comment on lines 45 to 49
hooks.beforeEach(() => addTranslations(locale, undefined));
}

if (translations) {
hooks.beforeEach(() => addTranslations(translations));
hooks.beforeEach(() => addTranslations(translations, undefined));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the explicit undefined arguments necessary?

I would expect the translations? to work correctly. Maybe it works with the suggested ...args change to makeIntlHelper? 🤔

export default makeIntlHelper((intl: IntlService, localeName: string | Translations, translations?: Translations) => { ... });

Comment on lines 1 to 6
// Types for compiled templates
declare module 'ember-intl/templates/*' {
import { TemplateFactory } from 'htmlbars-inline-precompile';
const tmpl: TemplateFactory;
export default tmpl;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need these types, as ember-intl has no templates.

among other PR feedback fixes
@kturney
Copy link
Collaborator Author

kturney commented Mar 1, 2020

Thanks for the feedback! I've

  • rebased on master
  • replaced most uses of any with unknown
  • bumped typescripty deps

@jasonmit jasonmit force-pushed the master branch 3 times, most recently from 3651c86 to 631b699 Compare March 28, 2020 04:05
@jasonmit
Copy link
Member

I'm still interesting in landing TS support but this has fallen a bit behind. Let me know if you want to re-attempt this and I'll try and hold off on any large changes.

@jasonmit jasonmit closed this Apr 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants