KEMBAR78
Introduction to typescript | PDF
Introduction to TypeScript
TS = JS + Types + Tooling … + FUN
SoCraTeN
Software Craftsmanship in Trentino
Software Developer @ ASA (hotel PMS software)
Particular interests:
● API design
● some functional programming
and a little category theory ;)
Programming Languages:
● Java
● JavaScript/TypeScript
Matthias Perktold
Mario A. Santini
Software Developer in Telecommunication field
like Free Software and solving problems
love to work with a lot of different languages:
JavaScript, Java, Rust, Go… and now TypeScript!
The web had needed a script language
World Wide Web was meant to share knowledge
not for applications
…
in order to be able to share you need a little help from
something more dynamic than just tags...
Why JavaScript
● the script for the web should have been small
● the script for the web should have no need to interact with anything outside
the browser
● you just need something to check forms data and react on events
That’s why pretty much all the other
existing languages don’t fit!
It was just...
<script>
alert(“Hello”);
</script>
...
<button
onClick=”document.forms.0.input[‘name’].value != ‘’”>
Submit
</button>
...
<script>
document.write(‘<p>Hello, World<blink>!</blink><br>’);
</script>
DHTML: it changed the world
● HTTP 1.1
● DOM API an extension to access in read/write to the
HTML document
● AJAX (a bit later)
JavaScript as a general purpose language
NodeJS
It was enough for the small part...
It is not for the wide one:
● Namespace: only global or function scope
● Code organization: no module/package nothing standard
● OO without classes: no design patterns reference
● Poor tooling
ECMAScript
● The standard moves faster and faster but only recently
● Browser vendors have to implement new specifications
● Cannot break the past
● Not all the issues are addressed
Then which alternatives
● Java Applet
● Flash
● CoffeScript
● GWT / Bridge.NET
● ZK
Recent alternatives
● Dart
● ClojureScript
● Elm
● Flow
● TypeScript
Why TypeScript?
● It is JavaScript with types
● All JavaScript code is already TypeScript
● Easy for Java / C# people
● Not intrusive
● Allows all the new cool stuff
● Sponsored by Google and used in Angular 2 >>
Why it make JavaScript better
● Strong types with dynamic inference
● You can define your own types
● You can use generics, with unions, intersection and
more…
● You don’t need to add all together
● You can exit any time with no a big deal
● Oh… and it made the tooling much more cool!
Type System
Type Annotations
export interface Named {
name: string;
}
export function greet({ name }: Named): string {
return `Hello, ${name}!`;
}
const john: Named = { name: "John" };
const greetingForJohn: string = greet(john);
const greetingForEmma = greet({ name: "Emma" });
document.body.innerHTML = `
<h2>${greetingForJohn}</h2>
<h2>${greetingForEmma}</h2>
`;
Compiled to ECMAScript 6:
export function greet({ name }) {
return `Hello, ${name}!`;
}
const john = { name: "John" };
const greetingForJohn = greet(john);
const greetingForEmma = greet({ name: "Emma" });
document.body.innerHTML = `
<h2>${greetingForJohn}</h2>
<h2>${greetingForEmma}</h2>
`;
Classes and More Types
import { greet } from "./type-annotations";
class Person {
public readonly name: string;
constructor(
public readonly firstName: string,
public readonly lastName: string,
public readonly interests: string[] = []
) {
this.name = `${firstName} ${lastName}`;
}
public isInterestedIn(topic: string): boolean {
const idx: number = this.interests.indexOf(topic);
return idx >= 0;
}
}
const john: Person = new Person(
"John", "Doe",
["Politics", "Sports", "Programming"]
);
john.firstName = "Fred";
// -> Error: Cannot assign to 'firstName'
because it is a read-only property.
type ShowFn = (arg: any) => void;
const log: ShowFn = arg => console.log(arg);
log(john);
// Structural Typing
// -> Person is assignable to Named
log(greet(john));
Inheritance and Access Modifiers
enum State { Initial, Started, Stopped };
interface LifeCycle {
readonly state: State;
start(): void;
stop(): void;
}
abstract class AbstractLifeCycle
implements LifeCycle
{
private _state = State.Initial;
get state() { return this._state; }
start() {
if (this.state === State.Started)
return;
this.onStart();
this._state = State.Started;
}
stop() { /* similar to start */ }
protected abstract onStart(): void;
protected abstract onStop(): void;
}
No package scope
No final classes or methods
Inheritance and Access Modifiers - cont.
class Clock extends AbstractLifeCycle {
private intervalID?: number; // ? denotes optional property
protected onStart() {
this.intervalID = setInterval(
() => console.log(new Date),
1000
);
}
protected onStop() {
clearInterval(this.intervalID);
}
}
const clock = new Clock();
clock.start();
The compiler ensures we override all abstract
methods and properties.
Function Overloads
function plus(n1: number, n2: number): number;
function plus(s1: string, s2: string): string;
function plus<T>(a1: T[], a2: T[]): T[];
function plus(a1: any, a2: any): any {
return typeof a1 === "number"
? a1 + a2
: a1.concat(a2);
}
const n = plus(1, 2); // 3
const s = plus("1", "2"); // "12"
const a = plus([1], [2]); // [1, 2]
const x = plus(1, "2"); // Compiler Error
Tuples
function minMax(nums: number[]): [number, number] {
return [Math.min(...nums), Math.max(...nums)];
}
const [minNums, maxNums] = minMax([1,2,3,4]); // [1, 4]
function partition<T>(arr: T[], pred: (v: T) => boolean): [T[], T[]] {
const pass: T[] = [];
const fail: T[] = [];
for (const v of arr)
(pred(v) ? pass : fail).push(v);
return [pass, fail];
}
const [evens, odds] = partition([1,2,3,4], x => x % 2 === 0);
console.log(evens); // 2, 4
console.log(odds); // 1, 3
Tuples are already part of JS
interface Point {
x: number;
y: number;
}
const point: Point = { x: 3, y: 5 };
const entries: [string, number][] = Object.entries(point); // [ ["x",3], ["y",5] ]
for (const [key, value] of entries)
console.log(`${key} = ${value}`);
// logs:
// x = 3
// y = 5
Union Types
function getInstant(): Date | number {
return Date.now();
}
const inst = getInstant();
const date: Date = typeof inst === "number"
? new Date(inst) // known to be number here
: inst; // known to be Date here
console.log(date.toLocaleDateString());
Intersection Types
function withName<T>(obj: T, name: string): T & Named {
return { ...obj, name };
}
function greetPoint(point: Point & Named): string {
return greet(point)
+ ` You are at ${point.x}/${point.y}.`;
}
const origin: Point = { x: 0, y: 0 };
console.log(greetPoint(withName(origin, "Origin")));
Nullability
declare function parseDate(str: string): Date | null;
const d1: Date = null; // Error
const d2: Date = parseDate("2000-01-01"); // Error
const d3: Date | null = parseDate("2000-01-01"); // OK
console.log(d3.toLocaleDateString()); // Error
if (d3)
console.log(d3.toLocaleDateString()); // OK
Always use compiler flag “strictNullChecks”!
Type Guards
function isPoint(obj: any): obj is Point {
return typeof obj === "object"
&& typeof obj.x === "number"
&& typeof obj.y === "number";
}
function invalidPoint(p: any): never {
throw new Error(`Invalid point: ${p}`);
}
const requirePoint = (p: unknown) => isPoint(p) ? p : invalidPoint(p);
const loaded: unknown = JSON.parse(localStorage.get("current_point"));
let currentPoint: Point;
currentPoint = loaded; // Error
currentPoint = requirePoint(loaded); // OK
Discriminated Unions
aka Tagged Unions, Algebraic Data Types
type Expr<V> = Constant<V> | Plus<V> | Times<V>;
interface Constant<V> { tag: "Constant", value: V }
interface Plus<V> { tag: "Plus", left: Expr<V>, right: Expr<V> }
interface Times<T> { tag: "Times", left: Expr<T>, right: Expr<T> }
function evalNum(expr: Expr<number>): number {
switch (expr.tag) {
case "Constant": return expr.value;
case "Plus": return evalNum(expr.left) + evalNum(expr.right);
case "Times": return evalNum(expr.left) * evalNum(expr.right);
}
}
Index Types and Mapped Types
type PointKeys = keyof Point; // "x" | "y"
type XOfPoint = Point["x"]; // number
function mapVals<T, V>(
obj: T,
fn: (v: T[keyof T]) => V
): {
[k in keyof T]: V
} {
const res: any = {};
for (const [k, v] of Object.entries(obj))
res[k] = fn(v);
return res;
}
const mapped = mapVals(
{ x: 1, y: 2 },
v => String(v)
); // { x: string, y: string }
console.log(mapped); // { x: "1", y: "2" }
Conditional Types
type NonNullable<T> = T extends null | undefined ? never : T;
function requireNonNull<T>(val: T): NonNullable<T> {
if (val == null)
throw new TypeError();
return val as NonNullable<T>;
}
const nonNullDate: Date = requireNonNull(parseDate("2000-01-01"));
console.log(nonNullDate.toLocaleDateString());
Conditional types distribute over union types:
NonNullable<Date | null>
== NonNullable<Date> | NonNullable<null>
== Date | never
== Date
Putting it Together
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Exclude<T, U> = T extends U ? never : T;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;
declare function omit<T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K>;
const pickedX = pick(point, "x"); // { x: number }
const omittedX = omit(point, "x"); // { y: number }
Limits of the type system
type Func<I, O> = (arg: I) => O;
// Works, but only for a limited number of arguments
function pipe<A, B, C>(f1: Func<A, B>, f2: Func<B, C>): Func<A, C>;
function pipe<A, B, C, D>(f1: Func<A, B>, f2: Func<B, C>, f3: Func<C, D>): Func<A, D>;
function pipe<A, B, C, D, E>(f1: Func<A, B>, f2: Func<B, C>, f3: Func<C, D>, f4: Func<D, E>): Func<A, D>;
function pipe<A, B, C, D, E, F>(f1: Func<A, B>, f2: Func<B, C>, f3: Func<C, D>, f4: Func<D, E>, f5: Func<E, F>):
Func<A, F>;
function pipe(...fns: Func<any, any>[]): Func<any, any> {
return arg => fns.reduce((res, f) => f(res), arg);
}
const fThenG = pipe(f, g); // x => g(f(x))
Limits of the type system 2
No higher-kinded types
// Collects the results of a list of actions.
// Like Promise.all(), but works for any Monad, not only Promise.
// Possible in Haskell, but not in TS
sequence :: Monad m => [m a] -> m [a]
Tooling
IDE
Visual Studio Code
Type Definitions
Declare types of JS code in separate files without changing the original code
Allows to work with native JS code in TS in a typesafe way
// greet.js (original JS code)
export function greet({ name }) {
return `Hello, ${name}!`;
}
// greet.d.ts (typings of greet.js)
export interface Named {
name: string;
}
export declare function greet({ name }: Named): string;
Definitely Typed
https://github.com/DefinitelyTyped/DefinitelyTyped
Typings repository for popular JS libraries
npm install --save-dev @types/jquery
Analyze JS code using TS
Language Server
Source: https://jaxenter.de/eclipse-als-ide-marktfuehrer-68956
Conclusion
Is not going to save the day / despite it introduces types, use of last specs and
improved the tooling
Adopting it has a cost:
it is another language / even not so intrusive
few people know it compared to Javascript / it’s growing, but meanwhile...
not all library are types ready / you can fix what you need easely
Typescript improved the tooling for Javascript too!
It is better than the other alternatives so far...
Conclusion 2
Pros
● only adds, doesn’t change
● type safety
● catches errors at compile time
● structured documentation
● types guide an API user
● facilitates tooling
Cons
● more to learn, hiring more difficult
● complexity
● doesn’t replace testing
● verbosity
● burden on the API implementor
● type system has its limits
Riferimenti
SoCraTeN http://www.socraten.org/
Where to start: http://www.typescriptlang.org/
TypeScript in 5 minutes: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
Manual: https://www.typescriptlang.org/docs/handbook/basic-types.html
Reference: https://github.com/microsoft/TypeScript/blob/master/doc/spec.md
TypeScript github: https://github.com/microsoft/TypeScript
An honest view: https://medium.com/javascript-scene/the-typescript-tax-132ff4cb175b

Introduction to typescript

  • 1.
    Introduction to TypeScript TS= JS + Types + Tooling … + FUN
  • 2.
  • 3.
    Software Developer @ASA (hotel PMS software) Particular interests: ● API design ● some functional programming and a little category theory ;) Programming Languages: ● Java ● JavaScript/TypeScript Matthias Perktold
  • 4.
    Mario A. Santini SoftwareDeveloper in Telecommunication field like Free Software and solving problems love to work with a lot of different languages: JavaScript, Java, Rust, Go… and now TypeScript!
  • 5.
    The web hadneeded a script language World Wide Web was meant to share knowledge not for applications … in order to be able to share you need a little help from something more dynamic than just tags...
  • 6.
    Why JavaScript ● thescript for the web should have been small ● the script for the web should have no need to interact with anything outside the browser ● you just need something to check forms data and react on events That’s why pretty much all the other existing languages don’t fit!
  • 7.
    It was just... <script> alert(“Hello”); </script> ... <button onClick=”document.forms.0.input[‘name’].value!= ‘’”> Submit </button> ... <script> document.write(‘<p>Hello, World<blink>!</blink><br>’); </script>
  • 8.
    DHTML: it changedthe world ● HTTP 1.1 ● DOM API an extension to access in read/write to the HTML document ● AJAX (a bit later)
  • 9.
    JavaScript as ageneral purpose language NodeJS
  • 10.
    It was enoughfor the small part... It is not for the wide one: ● Namespace: only global or function scope ● Code organization: no module/package nothing standard ● OO without classes: no design patterns reference ● Poor tooling
  • 11.
    ECMAScript ● The standardmoves faster and faster but only recently ● Browser vendors have to implement new specifications ● Cannot break the past ● Not all the issues are addressed
  • 12.
    Then which alternatives ●Java Applet ● Flash ● CoffeScript ● GWT / Bridge.NET ● ZK
  • 13.
    Recent alternatives ● Dart ●ClojureScript ● Elm ● Flow ● TypeScript
  • 14.
    Why TypeScript? ● Itis JavaScript with types ● All JavaScript code is already TypeScript ● Easy for Java / C# people ● Not intrusive ● Allows all the new cool stuff ● Sponsored by Google and used in Angular 2 >>
  • 15.
    Why it makeJavaScript better ● Strong types with dynamic inference ● You can define your own types ● You can use generics, with unions, intersection and more… ● You don’t need to add all together ● You can exit any time with no a big deal ● Oh… and it made the tooling much more cool!
  • 16.
  • 17.
    Type Annotations export interfaceNamed { name: string; } export function greet({ name }: Named): string { return `Hello, ${name}!`; } const john: Named = { name: "John" }; const greetingForJohn: string = greet(john); const greetingForEmma = greet({ name: "Emma" }); document.body.innerHTML = ` <h2>${greetingForJohn}</h2> <h2>${greetingForEmma}</h2> `; Compiled to ECMAScript 6: export function greet({ name }) { return `Hello, ${name}!`; } const john = { name: "John" }; const greetingForJohn = greet(john); const greetingForEmma = greet({ name: "Emma" }); document.body.innerHTML = ` <h2>${greetingForJohn}</h2> <h2>${greetingForEmma}</h2> `;
  • 18.
    Classes and MoreTypes import { greet } from "./type-annotations"; class Person { public readonly name: string; constructor( public readonly firstName: string, public readonly lastName: string, public readonly interests: string[] = [] ) { this.name = `${firstName} ${lastName}`; } public isInterestedIn(topic: string): boolean { const idx: number = this.interests.indexOf(topic); return idx >= 0; } } const john: Person = new Person( "John", "Doe", ["Politics", "Sports", "Programming"] ); john.firstName = "Fred"; // -> Error: Cannot assign to 'firstName' because it is a read-only property. type ShowFn = (arg: any) => void; const log: ShowFn = arg => console.log(arg); log(john); // Structural Typing // -> Person is assignable to Named log(greet(john));
  • 19.
    Inheritance and AccessModifiers enum State { Initial, Started, Stopped }; interface LifeCycle { readonly state: State; start(): void; stop(): void; } abstract class AbstractLifeCycle implements LifeCycle { private _state = State.Initial; get state() { return this._state; } start() { if (this.state === State.Started) return; this.onStart(); this._state = State.Started; } stop() { /* similar to start */ } protected abstract onStart(): void; protected abstract onStop(): void; } No package scope No final classes or methods
  • 20.
    Inheritance and AccessModifiers - cont. class Clock extends AbstractLifeCycle { private intervalID?: number; // ? denotes optional property protected onStart() { this.intervalID = setInterval( () => console.log(new Date), 1000 ); } protected onStop() { clearInterval(this.intervalID); } } const clock = new Clock(); clock.start(); The compiler ensures we override all abstract methods and properties.
  • 21.
    Function Overloads function plus(n1:number, n2: number): number; function plus(s1: string, s2: string): string; function plus<T>(a1: T[], a2: T[]): T[]; function plus(a1: any, a2: any): any { return typeof a1 === "number" ? a1 + a2 : a1.concat(a2); } const n = plus(1, 2); // 3 const s = plus("1", "2"); // "12" const a = plus([1], [2]); // [1, 2] const x = plus(1, "2"); // Compiler Error
  • 22.
    Tuples function minMax(nums: number[]):[number, number] { return [Math.min(...nums), Math.max(...nums)]; } const [minNums, maxNums] = minMax([1,2,3,4]); // [1, 4] function partition<T>(arr: T[], pred: (v: T) => boolean): [T[], T[]] { const pass: T[] = []; const fail: T[] = []; for (const v of arr) (pred(v) ? pass : fail).push(v); return [pass, fail]; } const [evens, odds] = partition([1,2,3,4], x => x % 2 === 0); console.log(evens); // 2, 4 console.log(odds); // 1, 3
  • 23.
    Tuples are alreadypart of JS interface Point { x: number; y: number; } const point: Point = { x: 3, y: 5 }; const entries: [string, number][] = Object.entries(point); // [ ["x",3], ["y",5] ] for (const [key, value] of entries) console.log(`${key} = ${value}`); // logs: // x = 3 // y = 5
  • 24.
    Union Types function getInstant():Date | number { return Date.now(); } const inst = getInstant(); const date: Date = typeof inst === "number" ? new Date(inst) // known to be number here : inst; // known to be Date here console.log(date.toLocaleDateString());
  • 25.
    Intersection Types function withName<T>(obj:T, name: string): T & Named { return { ...obj, name }; } function greetPoint(point: Point & Named): string { return greet(point) + ` You are at ${point.x}/${point.y}.`; } const origin: Point = { x: 0, y: 0 }; console.log(greetPoint(withName(origin, "Origin")));
  • 26.
    Nullability declare function parseDate(str:string): Date | null; const d1: Date = null; // Error const d2: Date = parseDate("2000-01-01"); // Error const d3: Date | null = parseDate("2000-01-01"); // OK console.log(d3.toLocaleDateString()); // Error if (d3) console.log(d3.toLocaleDateString()); // OK Always use compiler flag “strictNullChecks”!
  • 27.
    Type Guards function isPoint(obj:any): obj is Point { return typeof obj === "object" && typeof obj.x === "number" && typeof obj.y === "number"; } function invalidPoint(p: any): never { throw new Error(`Invalid point: ${p}`); } const requirePoint = (p: unknown) => isPoint(p) ? p : invalidPoint(p); const loaded: unknown = JSON.parse(localStorage.get("current_point")); let currentPoint: Point; currentPoint = loaded; // Error currentPoint = requirePoint(loaded); // OK
  • 28.
    Discriminated Unions aka TaggedUnions, Algebraic Data Types type Expr<V> = Constant<V> | Plus<V> | Times<V>; interface Constant<V> { tag: "Constant", value: V } interface Plus<V> { tag: "Plus", left: Expr<V>, right: Expr<V> } interface Times<T> { tag: "Times", left: Expr<T>, right: Expr<T> } function evalNum(expr: Expr<number>): number { switch (expr.tag) { case "Constant": return expr.value; case "Plus": return evalNum(expr.left) + evalNum(expr.right); case "Times": return evalNum(expr.left) * evalNum(expr.right); } }
  • 29.
    Index Types andMapped Types type PointKeys = keyof Point; // "x" | "y" type XOfPoint = Point["x"]; // number function mapVals<T, V>( obj: T, fn: (v: T[keyof T]) => V ): { [k in keyof T]: V } { const res: any = {}; for (const [k, v] of Object.entries(obj)) res[k] = fn(v); return res; } const mapped = mapVals( { x: 1, y: 2 }, v => String(v) ); // { x: string, y: string } console.log(mapped); // { x: "1", y: "2" }
  • 30.
    Conditional Types type NonNullable<T>= T extends null | undefined ? never : T; function requireNonNull<T>(val: T): NonNullable<T> { if (val == null) throw new TypeError(); return val as NonNullable<T>; } const nonNullDate: Date = requireNonNull(parseDate("2000-01-01")); console.log(nonNullDate.toLocaleDateString()); Conditional types distribute over union types: NonNullable<Date | null> == NonNullable<Date> | NonNullable<null> == Date | never == Date
  • 31.
    Putting it Together typePick<T, K extends keyof T> = { [P in K]: T[P]; }; type Exclude<T, U> = T extends U ? never : T; type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>; declare function omit<T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K>; const pickedX = pick(point, "x"); // { x: number } const omittedX = omit(point, "x"); // { y: number }
  • 32.
    Limits of thetype system type Func<I, O> = (arg: I) => O; // Works, but only for a limited number of arguments function pipe<A, B, C>(f1: Func<A, B>, f2: Func<B, C>): Func<A, C>; function pipe<A, B, C, D>(f1: Func<A, B>, f2: Func<B, C>, f3: Func<C, D>): Func<A, D>; function pipe<A, B, C, D, E>(f1: Func<A, B>, f2: Func<B, C>, f3: Func<C, D>, f4: Func<D, E>): Func<A, D>; function pipe<A, B, C, D, E, F>(f1: Func<A, B>, f2: Func<B, C>, f3: Func<C, D>, f4: Func<D, E>, f5: Func<E, F>): Func<A, F>; function pipe(...fns: Func<any, any>[]): Func<any, any> { return arg => fns.reduce((res, f) => f(res), arg); } const fThenG = pipe(f, g); // x => g(f(x))
  • 33.
    Limits of thetype system 2 No higher-kinded types // Collects the results of a list of actions. // Like Promise.all(), but works for any Monad, not only Promise. // Possible in Haskell, but not in TS sequence :: Monad m => [m a] -> m [a]
  • 34.
  • 36.
  • 37.
    Type Definitions Declare typesof JS code in separate files without changing the original code Allows to work with native JS code in TS in a typesafe way // greet.js (original JS code) export function greet({ name }) { return `Hello, ${name}!`; } // greet.d.ts (typings of greet.js) export interface Named { name: string; } export declare function greet({ name }: Named): string;
  • 38.
    Definitely Typed https://github.com/DefinitelyTyped/DefinitelyTyped Typings repositoryfor popular JS libraries npm install --save-dev @types/jquery
  • 39.
  • 40.
  • 41.
    Conclusion Is not goingto save the day / despite it introduces types, use of last specs and improved the tooling Adopting it has a cost: it is another language / even not so intrusive few people know it compared to Javascript / it’s growing, but meanwhile... not all library are types ready / you can fix what you need easely Typescript improved the tooling for Javascript too! It is better than the other alternatives so far...
  • 42.
    Conclusion 2 Pros ● onlyadds, doesn’t change ● type safety ● catches errors at compile time ● structured documentation ● types guide an API user ● facilitates tooling Cons ● more to learn, hiring more difficult ● complexity ● doesn’t replace testing ● verbosity ● burden on the API implementor ● type system has its limits
  • 43.
    Riferimenti SoCraTeN http://www.socraten.org/ Where tostart: http://www.typescriptlang.org/ TypeScript in 5 minutes: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html Manual: https://www.typescriptlang.org/docs/handbook/basic-types.html Reference: https://github.com/microsoft/TypeScript/blob/master/doc/spec.md TypeScript github: https://github.com/microsoft/TypeScript An honest view: https://medium.com/javascript-scene/the-typescript-tax-132ff4cb175b