Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Typed Ember extends Confidence

Krystan HuffMenne
March 30, 2021
86

Typed Ember extends Confidence

As a developer with a non-traditional background, I struggled with imposter syndrome as I climbed the ranks to senior developer. This is the story of how converting our 7-year-old legacy Ember app to TypeScript in the throes of a pandemic helped me build confidence and write code that JustWorks™️. With Octane and native classes, using TypeScript with Ember is more straightforward than ever. Learn why you too should consider moving to TypeScript and tips for a painless transition. Not interested in switching? We'll talk about how to harness some of TypeScript's benefits even without TypeScript.

Krystan HuffMenne

March 30, 2021
Tweet

Transcript

  1. Maya Angelou “Uh-oh, they’re going to find out now. I’ve

    run a game on everybody, and they're going to find me out.”
  2. TODO Surround yourself with supportive people. Own your accomplishments. Learn

    to take your mistakes in stride. See yourself as a work in progress. Train yourself not to need external validation. Realize there’s no shame in asking for help. Use positive a ffi rmations. Say “yes” to opportunities. Visualize success. Go to therapy. Do some yoga. Embrace feeling like an imposter. Decide to be con fi dent.
  3. What even is a “type”? • What kind of data

    • What you can and cannot do
  4. primitive-types.js typeof 1; //=> 'number' typeof 'Hello, EmberConf!'; //=> 'string'

    typeof true; //=> 'boolean' typeof 9007199254740991n; //=> 'bigint' typeof Symbol(); //=> 'symbol' typeof undefined; //=> 'undefined' typeof null; //=> 'object' null === null; //=> true 👋 handwave 👋 handwave Making typeof null === 'null' would break the internet Primitives
  5. Structural types structural/object.js typeof { hello: 'EmberConf!' }; //=> 'object'

    typeof ['Hello', 'EmberConf!']; //=> 'object' typeof new Set(['Hello', 'EmberConf!']); //=> 'object' typeof new Map([['Hello', 'EmberConf!']]); //=> 'object' class Tomster {} let tomster = new Tomster(); typeof tomster; //=> 'object' object
  6. 👋 handwave 👋 handwave For framework code or when using

    iFrames, you might not want to use instanceof either. Structural types structural/object.js class Tomster {} let tomster = new Tomster(); tomster instanceof Tomster; //=> true Array.isArray(['Hello', 'EmberConf!']); //=> true
  7. Structural types structural/function.js function hello(conf = 'EmberConf') { return `Hello,

    ${conf}!`; } typeof hello; //=> 'function' hello(); //=> 'Hello, EmberConf!' hello.call(this, 'RailsConf'); //=> 'Hello, RailsConf!' hello.hola = 'Hola!'; hello.hola; 'Hola!' object
  8. loosely-typed/reassignment.js let year = 2021; typeof year; //=> 'number' year

    = 'two thousand and twenty one'; typeof year; //=> 'string'; Loosey goosey
  9. loosely-typed/coercion.js 2 + 2; //=> 4 2 + '2'; //=>

    '22' 2 + [2]; //=> '22' 2 + true; //=> 3 2 + null; //=> 2 2 + undefined; //=> NaN 2 + new Set([2]); //=> '2[object Set]’ Loosey goosey 🤪
  10. My Unassuming JavaScript Website Thank you for coming to my

    website! I wrote some loosely typed and dynamic JavaScript. 2 + 2 = 22 LOLOL Here’s a cat pic. /\_/\ ( o.o ) > ^ < www.mywebsite.com A real dynamo Uncaught TypeError: Cannot read property 'oops' of undefined Uncaught TypeError: undefined is not a function Uncaught TypeError: Cannot set property 'foo' of undefined
  11. What could go wrong 1. Uncaught TypeError: Cannot read property

    'oops' of undefined 2. TypeError: 'undefined' is not an object (evaluating 'undefined.oops') 3. TypeError: null is not an object (evaluating 'null.oops') 5. TypeError: Object doesn’t support property or method 'oops' 6. Uncaught TypeError: undefined is not a function 8. Uncaught TypeError: Cannot read property 'length' of undefined 9. Uncaught TypeError: Cannot set property 'oops' of undefined 10. ReferenceError: oops is not defined 🥴
  12. TypeScript compiler (tsc) shout.ts function shout(message: string) { return message.toUpperCase();

    } shout('hello'); //=> 'HELLO' Terminal $ yarn tsc shout.ts shout.js (compiled) function shout(message) { return message.toUpperCase(); } shout('hello'); //=> 'HELLO'
  13. TypeScript compiler (tsc) shout.ts function shout(message: string) { return message.toUpperCase();

    } shout('hello'); //=> 'HELLO' shout(42); Terminal $ yarn tsc shout.ts $ yarn tsc shout.ts shout.ts:8:7 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. 8 shout(42); ~~ Found 1 error. shout.js (compiled) function shout(message) { return message.toUpperCase(); } shout('hello'); //=> ‘HELLO' shout(42); //=> Uncaught TypeError: message.toUpperCase is not a function
  14. Statically Typed Terminal $ yarn tsc shout.ts shout.ts:8:7 - error

    TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. 8 shout(42); ~~ Found 1 error.
  15. strict/coercion-allowed.ts let itemCount = 42; throw 'Too many items in

    queue! Item count: ' + itemCount; //=> Error: Too many items in queue! Item count: 42 Strict, but not TOO strict idiomatic: (adj.) everyone does it
  16. strict/coercion-allowed.ts Strict, but not TOO strict tscon fi g.json {

    "compilerOptions": { "noImplicitAny": true, "noImplicitThis": true, "alwaysStrict": true, "strictNullChecks": true, "strictPropertyInitialization": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true,
  17. primitives.ts let myVariable: number = 1; let myVariable: bigint =

    9007199254740991n; let myVariable: string = 'Hello, EmberConf!'; let myVariable: boolean = true; let myVariable: symbol = Symbol(); let myVariable: undefined; let myVariable: null = null; Primitives in TypeScript
  18. primitives.ts let myVariable = 1; let myVariable = 9007199254740991n; let

    myVariable = 'Hello, EmberConf!'; let myVariable = true; let myVariable = Symbol(); let myVariable; let myVariable = null; Primitives in TypeScript
  19. structural/array.ts let myVariable: Array<string> = ['Hello', 'EmberConf!']; Structural types in

    TypeScript generic: (adj.) a reusable type that takes another type as an argument
  20. structural/function.ts function sayHello(crowd: string): string { return `Hello, ${crowd}!`; }

    sayHello('EmberConf'); //=> 'Hello, EmberConf!' Structural types in TypeScript
  21. additional-types/unknown.ts function prettyPrint(raw: unknown): string { if (typeof raw ===

    'string') { // TypeScript now knows that `raw` is a string return raw; } if (Array.isArray(raw)) { // TypeScript now knows that `raw` is an array return raw.join(', '); } throw '`prettyPrint` not implemented for this type'; } Into the unknown… narrow: (verb) to re fi ne a type to a more speci fi c type
  22. Additional-types/any.ts let yolo: any = 'hehehe'; // TypeScript won't yell

    at you here yolo = null; // or here yolo.meaningOfLife; //=> TypeError: Cannot read property 'meaningOfLife' of null Into the abyss… Pro tip: You can disallow any with tsconfig and eslint-typescript rules!
  23. Terminal $ ember install ember-cli-typescript 🚧 Installing packages… ember-cli-typescript, typescript,

    @types/ember, @types/ember-data, Etc… create tsconfig.json create app/config/environment.d.ts create types/super-rentals/index.d.ts create types/ember-data/types/registries/model.d.ts create types/global.d.ts ember-cli-typescript
  24. Terminal $ ember install ember-cli-typescript 🚧 Installing packages… ember-cli-typescript, typescript,

    @types/ember, @types/ember-data, Etc… create tsconfig.json create app/config/environment.d.ts create types/super-rentals/index.d.ts create types/ember-data/types/registries/model.d.ts create types/global.d.ts Installed addon package. ember-cli-typescript
  25. tscon fi g.json { "compilerOptions": { // "alwaysStrict": true, //

    "noImplicitAny": true, // "noImplicitThis": true, // "strictBindCallApply": true, // "strictFunctionTypes": true, // "strictNullChecks": true, // "strictPropertyInitialization": true, // ... } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  26. tscon fi g.json { "compilerOptions": { "alwaysStrict": true, // "noImplicitAny":

    true, // "noImplicitThis": true, // "strictBindCallApply": true, // "strictFunctionTypes": true, // "strictNullChecks": true, // "strictPropertyInitialization": true, // ... } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  27. tscon fi g.json { "compilerOptions": { "alwaysStrict": true, "noImplicitAny": true,

    // "noImplicitThis": true, // "strictBindCallApply": true, // "strictFunctionTypes": true, // "strictNullChecks": true, // "strictPropertyInitialization": true, // ... } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  28. tscon fi g.json { "compilerOptions": { "alwaysStrict": true, "noImplicitAny": true,

    "noImplicitThis": true, // "strictBindCallApply": true, // "strictFunctionTypes": true, // "strictNullChecks": true, // "strictPropertyInitialization": true, // … } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  29. tscon fi g.json { "compilerOptions": { "alwaysStrict": true, "noImplicitAny": true,

    "noImplicitThis": true, "strictBindCallApply": true, // "strictFunctionTypes": true, // "strictNullChecks": true, // "strictPropertyInitialization": true, // ... } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  30. tscon fi g.json { "compilerOptions": { "alwaysStrict": true, "noImplicitAny": true,

    "noImplicitThis": true, "strictBindCallApply": true, "strictFunctionTypes": true, // "strictNullChecks": true, // "strictPropertyInitialization": true, // ... } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  31. tscon fi g.json { "compilerOptions": { "alwaysStrict": true, "noImplicitAny": true,

    "noImplicitThis": true, "strictBindCallApply": true, "strictFunctionTypes": true, "strictNullChecks": true, // "strictPropertyInitialization": true, // ... } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  32. tscon fi g.json { "compilerOptions": { "alwaysStrict": true, "noImplicitAny": true,

    "noImplicitThis": true, "strictBindCallApply": true, "strictFunctionTypes": true, "strictNullChecks": true, "strictPropertyInitialization": true, // ... } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  33. tscon fi g.json { "compilerOptions": { "strict": true, // ...

    } } Gradual strictness Or: How I Learned to Stop Worrying and Love the Strictness Less strict More strict
  34. Terminal $ yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin Gradual strictness Or:

    How I Learned to Stop Worrying and Love the Strictness Ludicrous mode! 🤯 Less strict More strict
  35. Where do we start? Outer Leaves Models Routes, Services, Adapters,

    Serializers Inner Leaves Components, Controllers
  36. Where do we start? Outer Leaves Models Routes, Services, Adapters,

    Serializers Inner Leaves Components, Controllers
  37. Where do we start? Outer Leaves Models Routes, Services, Adapters,

    Serializers Inner Leaves Components, Controllers 👉 👇 👈
  38. Where do we start? Outer Leaves Models Routes, Services, Adapters,

    Serializers Inner Leaves Components, Controllers 🥸 🤠 🤩 🤡
  39. Where do we start? Outer Leaves Models Routes, Services, Adapters,

    Serializers Inner Leaves Components, Controllers 🥸 🤠 🤩 🤡 👉 👇 👈
  40. app/models/rental.js import Model, { attr } from '@ember-data/model'; const COMMUNITY_CATEGORIES

    = ['Condo', 'Townhouse', 'Apartment']; export default class RentalModel extends Model { @attr title; @attr owner; @attr city; @attr location; @attr category; @attr image; @attr bedrooms; @attr description; get type() { if (COMMUNITY_CATEGORIES.includes(this.category)) { return 'Community'; } else { return 'Standalone'; } } } Models
  41. app/models/rental.js import Model, { attr } from '@ember-data/model'; const COMMUNITY_CATEGORIES

    = ['Condo', 'Townhouse', 'Apartment']; export default class RentalModel extends Model { @attr title; @attr owner; @attr city; @attr location; @attr category; @attr image; @attr bedrooms; @attr description; get type() { if (COMMUNITY_CATEGORIES.includes(this.category)) { return 'Community'; } else { return 'Standalone'; } } } Models
  42. app/models/rental.js import Model, { attr } from '@ember-data/model'; const COMMUNITY_CATEGORIES

    = ['Condo', 'Townhouse', 'Apartment']; export default class RentalModel extends Model { @attr title; @attr owner; @attr city; @attr location; @attr category; @attr image; @attr bedrooms; @attr description; get type() { if (COMMUNITY_CATEGORIES.includes(this.category)) { return 'Community'; } else { return 'Standalone'; } } } Models
  43. app/models/rental.js import Model, { attr } from '@ember-data/model'; const COMMUNITY_CATEGORIES

    = ['Condo', 'Townhouse', 'Apartment']; export default class RentalModel extends Model { @attr title; @attr owner; @attr city; @attr location; @attr category; @attr image; @attr bedrooms; @attr description; get type() { if (COMMUNITY_CATEGORIES.includes(this.category)) { return 'Community'; } else { return 'Standalone'; } } } Models Terminal $ mv app/models/rental.js app/models/rental.ts
  44. app/models/rental.ts import Model, { attr } from '@ember-data/model'; const COMMUNITY_CATEGORIES

    = ['Condo', 'Townhouse', 'Apartment']; export default class RentalModel extends Model { @attr title; @attr owner; @attr city; @attr location; @attr category; @attr image; @attr bedrooms; @attr description; get type() { if (COMMUNITY_CATEGORIES.includes(this.category)) { return 'Community'; } else { return 'Standalone'; } } } Models
  45. app/models/rental.ts Models public/api/rentals.json { "data": [ { "type": "rentals", "id":

    "grand-old-mansion", "attributes": { "title": "Grand Old Mansion", "owner": "Veruca Salt", "city": "San Francisco", "location": { "lat": 37.7749, "lng": -122.4194 }, "category": "Estate", "image": "https://upload.wikimedia.org/mansion.jpg", "bedrooms": 15, "description": "This grand old mansion sits..." } }, // ... ] }
  46. relationship-example.ts import Model, { AsyncBelongsTo, AsyncHasMany, belongsTo, hasMany, } from

    '@ember-data/model'; import Comment from 'my-app/models/comment'; import User from 'my-app/models/user'; export default class PostModel extends Model { @belongsTo('user') declare author: AsyncBelongsTo<User>; @hasMany('comments') declare comments: AsyncHasMany<Comment>; } Model Relationships
  47. app/routes/index.js import Route from '@ember/routing/route'; import { inject as service

    } from '@ember/service'; export default class IndexRoute extends Route { @service store; model() { return this.store.findAll('rental'); } } Routes
  48. app/routes/index.js import Route from '@ember/routing/route'; import { inject as service

    } from '@ember/service'; export default class IndexRoute extends Route { @service store; model() { return this.store.findAll('rental'); } } Routes Terminal $ mv app/routes/index.js app/routes/index.ts
  49. app/routes/index.ts Routes app/models/rental.ts import Model, { attr } from '@ember-data/model';

    export default class RentalModel extends Model { // ... } declare module 'ember-data/types/registries/model' { export default interface ModelRegistry { rental: RentalModel; } }
  50. app/components/rentals/ fi lter.js import Component from '@glimmer/component'; export default class

    RentalsFilterComponent extends Component { get results() { let { rentals, query } = this.args; if (query) { rentals = rentals.filter((rental) => rental.title.includes(query)); } return rentals; } } Components Terminal $ mv app/components/rentals/filter.js app/components/rentals/filter.ts
  51. app/components/rentals/ fi lter.ts Components app/components/rentals.hbs <Rentals::Filter @rentals={{@rentals}} @query={{this.query}} as |results|>

    {{#each results as |rental|}} <li><Rental @rental={{rental}} /></li> {{/each}} </Rentals::Filter> app/templates/index.hbs <Rentals @rentals={{@model}} />
  52. app/components/rentals.ts Components import Component from '@glimmer/component'; import { tracked }

    from '@glimmer/tracking'; import IndexRoute from 'super-rentals/routes'; import { ModelFrom } from 'super-rentals/types/util'; interface RentalsArgs { rentals: ModelFrom<IndexRoute>; } export default class RentalsComponent extends Component<RentalsArgs> { @tracked query = ''; }
  53. Components app/components/map.js import Component from '@glimmer/component'; import ENV from 'super-rentals/config/environment';

    export default class MapComponent extends Component { get src() { let { lng, lat } = this.args; return `${ENV.MAPBOX_URL}/${lng},${lat}`; } }
  54. Components app/components/map.js import Component from '@glimmer/component'; import ENV from 'super-rentals/config/environment';

    export default class MapComponent extends Component { get src() { let { lng, lat } = this.args; return `${ENV.MAPBOX_URL}/${lng},${lat}`; } } Terminal $ mv app/components/map.js app/components/map.ts
  55. app/components/map.js /** * @typedef {object} MapArgs * @property {number |

    undefined} lng * @property {number | undefined} lat */ /** @type {Component<MapArgs>} */ export default class MapComponent extends Component { /** @type {number} */ get lng() { assert('Please provide `lng` arg', this.args.lng); return this.args.lng; } /** @type {number} */ get lat() { assert('Please provide `lat` arg', this.args.lat); return this.args.lat; } /** @type {string} */ get src() { return `${ENV.MAPBOX_URL}/${this.lng},${this.lat}`; } } TS without TS JSDoc + VSCode
  56. app/components/map.js // @ts-check /** * @typedef {object} MapArgs * @property

    {number | undefined} lng * @property {number | undefined} lat */ /** @type {Component<MapArgs>} */ export default class MapComponent extends Component { /** @type {number} */ get lng() { assert('Please provide `lng` arg', this.args.lng); return this.args.lng; } /** @type {number} */ get lat() { assert('Please provide `lat` arg', this.args.lat); return this.args.lat; } /** @type {string} */ get src() { return `${ENV.MAPBOX_URL}/${this.lng},${this.lat}`; } } TS without TS JSDoc + VSCode