Egghead course: Practical Advanced TypeScript

  • link
  • author: Rares Matei

Improve Readability with TypeScript Numeric Separators when working with Large Numbers

1const amount = 1234567890; // is not very readable
2const readable_amount = 1_234_567_890; // better!

Make TypeScript Class Usage Safer with Strict Property Initialization

  • using the new strictPropertyInitialization property on the tsconfig.json, we can let typescript that properties on our classes are going to be initialized on construction.
1class Library {
2 titles!: string[]; // check the ! at the end of the property name
3}
4
5// there will not be a TS error here, but you might end up with runtime errors
6const shortTitles = library.titles.filter((title) => title.length < 5);

Use the JavaScript β€œin” operator for automatic type inference in TypeScript

1interface Admin {
2 id: string;
3 role: string:
4}
5interface User {
6 email: string;
7}
8
9function redirect(usr: Admin | User) {
10 if ("role" in usr) {
11 routeToAdminPage(usr.role);
12 } else {
13 routeToHomePage(usr.email);
14 }
15}

you can add an explicit type to an object, and infer the types of the other object properties by it. this is useful when dealing with switch statements, and make sure you are handling every case (using union types).

1export interface Action {
2 type: string;
3}
4
5export class Add implements Action {
6 readonly type = "Add"; //explicit "Add" type. is called the discriminate
7 constructor(public payload: string) {}
8}
9
10export class RemoveAll implements Action {
11 readonly type = "Remove All";
12}
13
14export type TodoActions = Add | RemoveAll; // union type. this is the finite state cases

Create Explicit and Readable Type Declarations with TypeScript mapped Type Modifiers

1interface IPet {
2 name: string;
3 age: number;
4 favoritePark?: string;
5}
6
7type ReadonlyPet = {
8 +readonly [K in keyof IPet]-?: IPet[K];
9};

let's explain the code above:

  • ReadonlyPet is a new type that is modifying all the properties from the IPet interface
  • it's setting all its properties to readonly (+readonly in the beginning, the + is optional)
  • it's also removing all the optional types from iPet (remove favoritePark)
  • [K in keyof IPet]: IPet[K] I guess is the mapped iterator part? πŸ€·β€β™‚οΈ

Use Types vs. Interfaces

The main difference between type aliases and interfaces are that you can build union types with type aliases but not with interface. an Interface is an specific contract, it cannot be one thing or another.

Another thing you can do with interfaces are define different with the same name. this will result in a merge between the two. That's why you can locally extend an interface (using a typings.d.ts file for example). So make sure when you are creating a library, all the public types must be implemented with interfaces and not type aliases.

1// ❌
2type Foo = {
3 a: string;
4};
5
6type Foo = {
7 b: string;
8};
1// βœ…
2interface Foo {
3 a: string;
4}
5
6interface Foo {
7 b: string;
8}
9
10let foo: Foo;
11foo.
1// ❌
2type PetType = IDog | ICat;
3
4// not possible to extend from a union type
5interface IPet extends PetType {}
6
7class Pet implements PetType {}
8
9interface IDog {}
10interface ICat {}

Build self-referencing type aliases in TypeScript

1interface TreeNode<T> {
2 value: T;
3 left: TreeNode<T>;
4 right: TreeNode<T>;
5}

Use the TypeScript "unknown" type to avoid runtime errors

any type is the most loose type in TS, it will lead to lots of errors the type unknown works better because it will only accept assertions when you check types in the code


Dynamically Allocate Function Types with Conditional Types in TypeScript

You can conditionally add types to properties in your interfaces, using a ternary operator on the type declaration

1type Item<T> = {
2 id: T;
3 container: T extends string ? StringContainer : NumberContainer;
4};

You can even filter types:

1type ArrayFilter<T> = T extends any[] ? T : never;
2
3type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;
4// StringsOrNumbers type now is string[] | number[] (it filtered all the non-array types)

Another examples is to lock down the types that a function can accept like the example below:

1interface IItemService {
2 getItem<T extends string | number>(id: T): T extends string ? Book : Tv;
3}
4
5// `<T extends string | number>` will only let the generic to be extended from `string` and `number`
6
7let itemService: IItemService;
8
9const book = itemService.getItem("10");
10const tv = itemService.getItem(true); // TS will complain in this case

Generics + conditionals are super powerful

1const numbers = [2, 1]; // --> number[]
2
3const someObject = {
4 id: 21,
5 name: 'Jonathan'
6};
7
8const someBoolean = true;
9
10type Flatten<T> = T extends any [] ? T[number];
11 T extends object ? T[keyof T];
12 T;
13
14// keyof T --> "id" | "name"
15// T["id" | "name"] --> T["id"] | T["name"] --> number | string
16
17type NumbersArrayFlattened = Flatten<typeof numbers>; // --> number
18type SomeObjectFlattened = Flatten<typeof someObject>; // --> number | string
19type SomeBooleanFlattened = Flatten<typeof someBoolean>; // --> true

Infer the Return Type of a Generic Function Type Parameter https://egghead.io/lessons/typescript-infer-the-return-type-of-a-generic-function-type-parameter

1function generateId(seed: number) {
2 return seed + 5;
3}
4
5type ReturnType<T> = T extends (...args: any[]) => R ? R : any;
6type Id = ReturnType<typeof generateId>;
7
8lookupEntity(generateId(10));
9
10function lookupEntity(id: string) {
11 // query DB for entity by ID
12}

Deeply mark all the properties of a type as read-only in TypeScript

1type DeepReadonlyObject<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };
2// this applies `readonly` to all the attrs to an object and then recursively calls it to its values
3
4type DeepReadonly<T> = T extends (infer E)[]
5 ? ReadonlyArray<ReadonlyArray<DeepReadonlyObject<E>>>
6 : T extends object
7 ? DeepReadonlyObject<T>
8 : T;
9
10// this is a conditional type that checks if the tyoe is an array so we can call non-mutable methods to the array (map, filter...)
11
12type IReadonlyRootState = DeepReadonly<IRootState>;

Dynamically initialize class properties using TypeScript decorators

Decorators are a powerful feature of TypeScript that allow for efficient and readable abstractions when used correctly. In this lesson we will look at how we can use decorators to initialize properties of a class to promises that will make GET requests to certain URLs. We will also look at chaining multiple decorators to create powerful and versatile abstractions.

1function First() {
2 return function (target: any, name: string) {
3 const hiddenInstanceKey = "_$$" + name + "$$_";
4 const prevInit = Object.getOwnPropertyDescriptor(target, name).get;
5 const init = () => {
6 return prevInit().then((response) => response[0]);
7 };
8
9 Object.defineProperty(target, name, {
10 get: function () {
11 return this[hiddenInstanceKey] || (this[hiddenInstanceKey] = init());
12 },
13 configurable: true,
14 });
15 };
16}
17
18class TodoService {
19 @First() // second decorator
20 @GetTodos("https://jsonplaceholder.typicode.com/todos") // first decorator!
21 todos: Promise<ITodo[]>;
22}

decorators are called from bottom to top!

Β© 2022, Powered by Gatsby β€’