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 readable2const readable_amount = 1_234_567_890; // better!
Make TypeScript Class Usage Safer with Strict Property Initialization
- using the new
strictPropertyInitialization
property on thetsconfig.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 name3}45// there will not be a TS error here, but you might end up with runtime errors6const 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}89function 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}45export class Add implements Action {6 readonly type = "Add"; //explicit "Add" type. is called the discriminate7 constructor(public payload: string) {}8}910export class RemoveAll implements Action {11 readonly type = "Remove All";12}1314export 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}67type 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 theIPet
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
(removefavoritePark
) [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};56type Foo = {7 b: string;8};
1// β
2interface Foo {3 a: string;4}56interface Foo {7 b: string;8}910let foo: Foo;11foo.
1// β2type PetType = IDog | ICat;34// not possible to extend from a union type5interface IPet extends PetType {}67class Pet implements PetType {}89interface 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;23type 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}45// `<T extends string | number>` will only let the generic to be extended from `string` and `number`67let itemService: IItemService;89const 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[]23const someObject = {4 id: 21,5 name: 'Jonathan'6};78const someBoolean = true;910type Flatten<T> = T extends any [] ? T[number];11 T extends object ? T[keyof T];12 T;1314// keyof T --> "id" | "name"15// T["id" | "name"] --> T["id"] | T["name"] --> number | string1617type NumbersArrayFlattened = Flatten<typeof numbers>; // --> number18type SomeObjectFlattened = Flatten<typeof someObject>; // --> number | string19type 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}45type ReturnType<T> = T extends (...args: any[]) => R ? R : any;6type Id = ReturnType<typeof generateId>;78lookupEntity(generateId(10));910function lookupEntity(id: string) {11 // query DB for entity by ID12}
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 values34type DeepReadonly<T> = T extends (infer E)[]5 ? ReadonlyArray<ReadonlyArray<DeepReadonlyObject<E>>>6 : T extends object7 ? DeepReadonlyObject<T>8 : T;910// 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...)1112type 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 };89 Object.defineProperty(target, name, {10 get: function () {11 return this[hiddenInstanceKey] || (this[hiddenInstanceKey] = init());12 },13 configurable: true,14 });15 };16}1718class TodoService {19 @First() // second decorator20 @GetTodos("https://jsonplaceholder.typicode.com/todos") // first decorator!21 todos: Promise<ITodo[]>;22}
decorators are called from bottom to top!