# Study Group ## Agenda Item 49: Provide a Type for this in Callbacks Item 50: Prefer Conditional Types to Overloaded Declarations Item 51: Mirror Types to Sever Dependencies ### Item 49: Provide a Type for *this* in Callbacks #### What is *this* keyword Dynamically scoped: its value depends not on the way in which it was *defined* but on the way in which it was *called* Most often used in class => refers to current instance of the class object ```javascript=1 class ExampleClass { vals = []; constructor(vals) { this.vals = vals; } logSquares() { for (const val of this.vals) { console.log(val * val) } } } const a = new ExampleClass([1,2,3]); a.logSquares(); // 1 4 9 const b = new ExampleClass([2,3,4]); b.logSquares(); // 4, 9, 16 ``` When used within an object => refers to the current object ```javascript=1 const exampleObject = { vals: [1,2,3], logSquares: function() { for(const val of this.vals) { console.log(val * val) } } } exampleObject.logSquares(); // 1 4 9 ``` When used within a regular function => refers to the window object ```javascript=1 function exampleFunc() { console.log(this); // window object } ``` JS provides *bind, call, apply* funcs, so that the programmer can determine the value of *this* #### Using ExampleClass Example from Above Now, look what happens when we assign the `logSqaure` function to a variable: ```javascript=1 const a = new ExampleClass([1,2,3]); const func = a.logSquares; func(); // Uncaught TypeError: Cannot read property 'vals' of undefined ``` :::spoiler What's going on here? `a.logSquares` actually does the following 1. calls `ExampleClass.prototype.logSquares` 2. binds the value of *this* in that function to the variable *a* ::: :::spoiler Solution 1. use bind in constructor to bind *this* to the class instance 2. rewrite `logSquares` as an arrow func 3. use call(apply) to explicitly set *this* ::: #### Another common example ```javascript=1 document.querySelector('input') .addEventListener('change', function(e) { console.log(this); }); ``` :::spoiler What is *this* going to be? Input element on which the event fired. ::: #### What Does *this* Keyword Have to Do with TypeScript? Because *this* binding is part of JavaScript, TypeScript models it. This means that if you’re writing (or typing) a library that sets the value of *this* on callbacks, then you should model *this*, too. ```javascript=1 function addKeyListener( el: HTMLElement, fn: (this: HTMLElement, e: KeyboardEvent) => void){ el.addEventListener('keydown', e => { fn.call(el, e); }); } declare let el: HTMLElement; addKeyListener(el, function(e) { this.innerHTML; // OK, "this" has type of HTMLElement }); ``` Note: *this* parameter is special: it’s not just another positional argument. ```javascript=1 class Foo { registerHandler(el: HTMLElement) { addKeyListener(el, e => { this.innerHTML; }); } } ``` :::spoiler Wrong Implementation :::danger If you use an arrow function here, you’ll override the value of *this*. TypeScript will catch the issue ::: #### Takeaways 1. Understand how *this* binding works 2. Provide a type for *this* in callbacks when it's part of the API ### Item 50: Prefer Conditional Types to Overloaded Declarations #### How Would You Write Type Declaration for the Following ```javascript=1 function double(x) { return x + x; } ``` The function, `double`, could be passed as a string or a number, so we could use union type and TypeScript's function overloading to achieve type declaration ```javascript=1 function double(x: string|number): string|number; function double(x: any) { return x + x; } const num = double(12) // string | number const str = double('x') // string | number ``` This declaration misses the subtle difference and will produce types that are hard to work with Might also write using generic ```javascript=1 function double<T extends string | number>(x: T): T; function double(x: any) { return x + x; }; const num = double(12) // 12 const str = double('x') // 'x' ``` Too precise! When pass a `string literal`, the type is the same `string literal` type. By passing a `string literal` type `'x'`, the result type should not be `'x'` because 'xx' should be a type string Another option is to provide multiple type declarations (Note: TypeScripts allows you to write any number of type declarations, but only **ONE** implementation of the function) ```javascript=1 function double(x: number): number; function double(x: string): string; function double(x: any) { return x + x; }; const num = double(12); // number const str = double('x'); // string ``` :::spoiler FINALLY ```javascript=1 function f(x : number | string) { double(x); // ERROR!!! } ``` NO, it will introduce a bug when double is being used in another function call that passes a `string` or a `number` as param When overload type declaration, TypeScript checks from top to bottom TypeScript will complain at the last overload because `string | number` is not assignable to `string` You may wonder you could have added another type overload for the case of `string | number`, but that is not ideal The best solution for this case is to use a `conditional type` ::: #### Conditional Types like if statements in type space ```javascript=1 function double<T extends number | string>(x : T): T extends string ? string : number; function double(x: any) { return x + x; } ``` Similar with using a generic, but with a more elaborate return type Read the return type as you would when using a ternary operator (?:) 1. If `T` is a subset of `string` (`string` or `string literal(s)`), then return type is `string` 2. Otherwise, return type is `number` #### Takeaways 1. Conditional types is a technique that can achieve type distributions with overloaded type declarations ### Item 51: Mirror Types to Sever Dependencies Given the following scenario: - Written a library for parsing CSV files - Function is to pass in contents of CSV and get back a parsed object - For the convenience for NodeJS users, you allow the contents to be either a `string` or a `NodeJS Buffer` ```javascript=1 function parseCSV(contents: string | Buffer): {[column: string]: string}[] { if (typeof contents === 'object') { // It's a buffer return parseCSV(contents.toString('utf8')); } // ... } ``` The type definition for Buffer comes from the NodeJS type declarations, so must install `npm install --save-dev @types/node` Since type declarations depend on the NodeJS types, you include `@types/node` as a `devDependency` :::spoiler Users will start complaining - JavaScript developers (what is the @type module doing?) - TypeScript developers (why depending on NodeJS?) Their concerns are reasonable because 1. `Buffer` behavior isn't essential 2. Only relevant for users who are using NodeJS 3. `@types/node` is only relavant to NodeJS users who are also using TypeScipt ::: #### Solution TypeScript's structural typing is a solution for the scenario Rather tahn using the declaration of `Buffer` from `@types/node`, can write your own with just the methods and properties you need In this case, we just need a toString method that takes an encoding parameter ```javascript=1 interface CsvBuffer { toString(encoding: string) : string; } function parseCSV(contents: string | CsvBuffer): {[column: string]: string}[] { // ... } ``` The interface is shorter than the complete one, but it does what the code needs from a `Buffer` In NodeJS project will work as well because types are compatible ```javascript=1 parseCSV(new Buffer("column1,column2\nval1,val2", "utf-8")) // OK ``` If your library only depends on the types for another library, might consider just mirroring the declarations you need If you depend on the implementation of a lib, still may be able to apply the same trick to avoid depending on its typings However, becomes difficult when dependence grows larger and more essential - In this case, make sure you formalize the relationship by making the @types dependency explicit This technique is also helpful for severing dependencies between UT and prod ```javascript=1 // Query results from a PostgresDB interface Author { first: string; last: string; } function getAuthors(database: PostgresDB): Author[] { const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`); return authorRos.map(row => ({ first: row[0], last: row[1] })); } // To test this, have to mock PostgresDB. // Better approach with structural typing interface DB { runQuery: (sql: string) => any[]; } function getAuthors(database: DB): Author[]{ const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`); return authorRows.map(row => ({ first: row[0], last: row[1] })); } // Test test('getAuthors', () => { const authors = getAuthors({ runQuery(sql: string) { return [['Toni', 'Morrison'], ['Maya', 'Angelou']]; } }); expect(authors).toEqual([ { first: 'Toni', last: 'Morrison' }, { first: 'Maya', last: 'Angelou' } ]); }); ``` #### Takeaways 1. Use structrual typing to sever dependencies that are trivial 2. Don't force JS users to depend on @types 3. Don't force web developers to depend on NodeJS