# 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