The Concise TypeScript Book PDF

Summary

The book "The Concise TypeScript Book" by Simone Poggiali is a complete overview of the TypeScript language. It covers type systems, advanced features, and getting started with the language. This open-source guide is a valuable resource for all TypeScript developers.

Full Transcript

The Concise TypeScript Book Simone Poggiali The Concise TypeScript Book The Concise TypeScript Book provides a comprehensive and succinct overview of TypeScript’s capabilities. It offers clear explanations covering all aspects found in the latest version of the language, from its powerful type sys...

The Concise TypeScript Book Simone Poggiali The Concise TypeScript Book The Concise TypeScript Book provides a comprehensive and succinct overview of TypeScript’s capabilities. It offers clear explanations covering all aspects found in the latest version of the language, from its powerful type system to advanced features. Whether you’re a beginner or an experienced developer, this book is an invaluable resource to enhance your understanding and proficiency in TypeScript. This book is completely Free and Open Source. Translations This book has been translated into several language versions, including: Chinese Downloads You can also download the Epub version here: https://github.com/gibbok/typescript-book/tree/main/downloads Table of Contents The Concise TypeScript Book Translations Downloads Table of Contents Introduction About the Author TypeScript Introduction What is TypeScript? Why TypeScript? TypeScript and JavaScript TypeScript Code Generation Modern JavaScript Now (Downleveling) Getting Started With TypeScript Installation Configuration TypeScript Configuration File ​tsconfig.json target lib strict module moduleResolution esModuleInterop jsx skipLibCheck files include exclude importHelpers Migration to TypeScript Advice Exploring the Type System The TypeScript Language Service Structural Typing TypeScript Fundamental Comparison Rules Types as Sets Assign a type: Type Declarations and Type Assertions Type Declaration Type Assertion Ambient Declarations Property Checking and Excess Property Checking Weak Types Strict Object Literal Checking (Freshness) Type Inference More Advanced Inferences Type Widening Const Const Modifier on Type Parameters Const assertion Explicit Type Annotation Type Narrowing Conditions Throwing or returning Discriminated Union User-Defined Type Guards Primitive Types string boolean number bigInt Symbol null and undefined Array any Type Annotations Optional Properties Readonly Properties Index Signatures Extending Types Literal Types Literal Inference strictNullChecks Enums Numeric enums String enums Constant enums Reverse mapping Ambient enums Computed and constant members Narrowing typeof type guards Truthiness narrowing Equality narrowing In Operator narrowing instanceof narrowing Assignments Control Flow Analysis Type Predicates Discriminated Unions The never Type Exhaustiveness checking Object Types Tuple Type (Anonymous) Named Tuple Type (Labeled) Fixed Length Tuple Union Type Intersection Types Type Indexing Type from Value Type from Func Return Type from Module Mapped Types Mapped Type Modifiers Conditional Types Distributive Conditional Types infer Type Inference in Conditional Types Predefined Conditional Types Template Union Types Any type Unknown type Void type Never type Interface and Type Common Syntax Basic Types Objects and Interfaces Union and Intersection Types Built-in Type Primitives Common Built-in JS Objects Overloads Merging and Extension Differences between Type and Interface Class Class Common Syntax Constructor Private and Protected Constructors Access Modifiers Get & Set Auto-Accessors in Classes this Parameter Properties Abstract Classes With Generics Decorators Class Decorators Property Decorator Method Decorator Getter and Setter Decorators Decorator Metadata Inheritance Statics Property initialization Method overloading Generics Generic Type Generic Classes Generic Constraints Generic contextual narrowing Erased Structural Types Namespacing Symbols Triple-Slash Directives Type Manipulation Creating Types from Types Indexed Access Types Utility Types Awaited Partial Required Readonly Record Pick Omit Exclude Extract NonNullable Parameters ConstructorParameters ReturnType InstanceType ThisParameterType OmitThisParameter ThisType Uppercase Lowercase Capitalize Uncapitalize Others Errors and Exception Handling Mixin classes Asynchronous Language Features Iterators and Generators TsDocs JSDoc Reference @types JSX ES6 Modules ES7 Exponentiation Operator The for-await-of Statement New.target Dynamic Import Expressions “tsc –watch” Non-null Assertion Operator (Postfix !) Defaulted declarations Optional Chaining Nullish coalescing operator (??) Template Literal Types Function overloading Recursive Types Recursive Conditional Types ECMAScript Module Support in Node.js Assertion Functions Variadic Tuple Types Boxed types Covariance and Contravariance in TypeScript Optional Variance Annotations for Type Parameters Template String Pattern Index Signatures The satisfies Operator Type-Only Imports and Export using declaration and Explicit Resource Management await using declaration ## Introduction Welcome to The Concise TypeScript Book! This guide equips you with essential knowledge and practical skills for effective TypeScript development. Discover key concepts and techniques to write clean, robust code. Whether you’re a beginner or an experienced developer, this book serves as both a comprehensive guide and a handy reference for leveraging TypeScript’s power in your projects. This book covers TypeScript 5.2. About the Author Simone Poggiali is an experienced Senior Front-end Developer with a passion for writing professional-grade code since the 90s. Throughout his international career, he has contributed to numerous projects for a wide range of clients, from startups to large organizations. Notable companies such as HelloFresh, Siemens, O2, and Leroy Merlin have benefited from his expertise and dedication. You can reach Simone Poggiali on the following platforms: LinkedIn: https://www.linkedin.com/in/simone-poggiali GitHub: https://github.com/gibbok Twitter: https://twitter.com/gibbok_coding Email: gibbok.coding📧gmail.com TypeScript Introduction What is TypeScript? TypeScript is a strongly typed programming language that builds on JavaScript. It was originally designed by Anders Hejlsberg in 2012 and is currently developed and maintained by Microsoft as an open source project. TypeScript compiles to JavaScript and can be executed in any JavaScript engine (e.g., a browser or server Node.js). TypeScript supports multiple programming paradigms such as functional, generic, imperative, and object-oriented. TypeScript is neither an interpreted nor a compiled language. Why TypeScript? TypeScript is a strongly typed language that helps prevent common programming mistakes and avoid certain kinds of run-time errors before the program is executed. A strongly typed language allows the developer to specify various program constraints and behaviors in the data type definitions, facilitating the ability to verify the correctness of the software and prevent defects. This is especially valuable in large-scale applications. Some of the benefits of TypeScript: Static typing, optionally strongly typed Type Inference Access to ES6 and ES7 features Cross-Platform and Cross-browser Compatibility Tooling support with IntelliSense TypeScript and JavaScript TypeScript is written in.ts or.tsx files, while JavaScript files are written in.js or.jsx. Files with the extension.tsx or.jsx can contain JavaScript Syntax Extension JSX, which is used in React for UI development. TypeScript is a typed superset of JavaScript (ECMAScript 2015) in terms of syntax. All JavaScript code is valid TypeScript code, but the reverse is not always true. For instance, consider a function in a JavaScript file with the.js extension, such as the following: const sum = (a, b) => a + b; The function can be converted and used in TypeScript by changing the file extension to.ts. However, if the same function is annotated with TypeScript types, it cannot be executed in any JavaScript engine without compilation. The following TypeScript code will produce a syntax error if it is not compiled: const sum = (a: number, b: number): number => a + b; TypeScript was designed to detect possible exceptions that can occur at runtime during compilation time by having the developer define the intent with type annotations. In addition, TypeScript can also catch issues if no type annotation is provided. For instance, the following code snippet does not specify any TypeScript types: const items = [{ x: 1 }, { x: 2 }]; const result = items.filter(item => item.y); In this case, TypeScript detects an error and reports: Property 'y' does not exist on type '{ x: number; }'. TypeScript’s type system is largely influenced by the runtime behavior of JavaScript. For example, the addition operator (+), which in JavaScript can either perform string concatenation or numeric addition, is modeled in the same way in TypeScript: const result = '1' + 1; // Result is of type string The team behind TypeScript has made a deliberate decision to flag unusual usage of JavaScript as errors. For instance, consider the following valid JavaScript code: const result = 1 + true; // In JavaScript, the result is equal 2 However, TypeScript throws an error: Operator '+' cannot be applied to types 'number' and 'boolean'. This error occurs because TypeScript strictly enforces type compatibility, and in this case, it identifies an invalid operation between a number and a boolean. TypeScript Code Generation The TypeScript compiler has two main responsibilities: checking for type errors and compiling to JavaScript. These two processes are independent of each other. Types do not affect the execution of the code in a JavaScript engine, as they are completely erased during compilation. TypeScript can still output JavaScript even in the presence of type errors. Here is an example of TypeScript code with a type error: const add = (a: number, b: number): number => a + b; const result = add('x', 'y'); // Argument of type 'string' is not assignable to parameter of type 'number'. However, it can still produce executable JavaScript output: 'use strict'; const add = (a, b) => a + b; const result = add('x', 'y'); // xy It is not possible to check TypeScript types at runtime. For example: interface Animal { name: string; } interface Dog extends Animal { bark: () => void; } interface Cat extends Animal { meow: () => void; } const makeNoise = (animal: Animal) => { if (animal instanceof Dog) { // 'Dog' only refers to a type, but is being used as a value here. //... } }; As the types are erased after compilation, there is no way to run this code in JavaScript. To recognize types at runtime, we need to use another mechanism. TypeScript provides several options, with a common one being “tagged union”. For example: interface Dog { kind: 'dog'; // Tagged union bark: () => void; } interface Cat { kind: 'cat'; // Tagged union meow: () => void; } type Animal = Dog | Cat; const makeNoise = (animal: Animal) => { if (animal.kind === 'dog') { animal.bark(); } else { animal.meow(); } }; const dog: Dog = { kind: 'dog', bark: () => console.log('bark'), }; makeNoise(dog); The property “kind” is a value that can be used at runtime to distinguish between objects in JavaScript. It is also possible for a value at runtime to have a type different from the one declared in the type declaration. For instance, if the developer has misinterpreted an API type and annotated it incorrectly. TypeScript is a superset of JavaScript, so the “class” keyword can be used as a type and value at runtime. class Animal { constructor(public name: string) {} } class Dog extends Animal { constructor( public name: string, public bark: () => void ) { super(name); } } class Cat extends Animal { constructor( public name: string, public meow: () => void ) { super(name); } } type Mammal = Dog | Cat; const makeNoise = (mammal: Mammal) => { if (mammal instanceof Dog) { mammal.bark(); } else { mammal.meow(); } }; const dog = new Dog('Fido', () => console.log('bark')); makeNoise(dog); In JavaScript, a “class” has a “prototype” property, and the “instanceof” operator can be used to test if the prototype property of a constructor appears anywhere in the prototype chain of an object. TypeScript has no effect on runtime performance, as all types will be erased. However, TypeScript does introduce some build time overhead. Modern JavaScript Now (Downleveling) TypeScript can compile code to any released version of JavaScript since ECMAScript 3 (1999). This means that TypeScript can transpile code from the latest JavaScript features to older versions, a process known as Downleveling. This allows the usage of modern JavaScript while maintaining maximum compatibility with older runtime environments. It’s important to note that during transpilation to an older version of JavaScript, TypeScript may generate code that could incur a performance overhead compared to native implementations. Here are some of the modern JavaScript features that can be used in TypeScript: ECMAScript modules instead of AMD-style “define” callbacks or CommonJS “require” statements. Classes instead of prototypes. Variables declaration using “let” or “const” instead of “var”. “for-of” loop or “.forEach” instead of the traditional “for” loop. Arrow functions instead of function expressions. Destructuring assignment. Shorthand property/method names and computed property names. Default function parameters. By leveraging these modern JavaScript features, developers can write more expressive and concise code in TypeScript. Getting Started With TypeScript Installation Visual Studio Code provides excellent support for the TypeScript language but does not include the TypeScript compiler. To install the TypeScript compiler, you can use a package manager like npm or yarn: npm install typescript --save-dev or yarn add typescript --dev Make sure to commit the generated lockfile to ensure that every team member uses the same version of TypeScript. To run the TypeScript compiler, you can use the following commands npx tsc or yarn tsc It is recommended to install TypeScript project-wise rather than globally, as it provides a more predictable build process. However, for one-off occasions, you can use the following command: npx tsc or installing it globally: npm install -g typescript If you are using Microsoft Visual Studio, you can obtain TypeScript as a package in NuGet for your MSBuild projects. In the NuGet Package Manager Console, run the following command: Install-Package Microsoft.TypeScript.MSBuild During the TypeScript installation, two executables are installed: “tsc” as the TypeScript compiler and “tsserver” as the TypeScript standalone server. The standalone server contains the compiler and language services that can be utilized by editors and IDEs to provide intelligent code completion. Additionally, there are several TypeScript-compatible transpilers available, such as Babel (via a plugin) or swc. These transpilers can be used to convert TypeScript code into other target languages or versions. Configuration TypeScript can be configured using the tsc CLI options or by utilizing a dedicated configuration file called tsconfig.json placed in the root of the project. To generate a tsconfig.json file prepopulated with recommended settings, you can use the following command: tsc --init When executing the tsc command locally, TypeScript will compile the code using the configuration specified in the nearest tsconfig.json file. Here are some examples of CLI commands that run with the default settings: tsc main.ts // Compile a specific file (main.ts) to JavaScript tsc src); Notes: Const Enums have hardcoded values, erasing the Enum, which can be more efficient in self-contained libraries but is generally not desirable. Also, Const enums cannot have computed members. Reverse mapping In TypeScript, reverse mappings in Enums refer to the ability to retrieve the Enum member name from its value. By default, Enum members have forward mappings from name to value, but reverse mappings can be created by explicitly setting values for each member. Reverse mappings are useful when you need to look up an Enum member by its value, or when you need to iterate over all the Enum members. Note that only numeric enums members will generate reverse mappings, while String Enum members do not get a reverse mapping generated at all. The following enum: enum Grade { A = 90, B = 80, C = 70, F = 'fail', } Compiles to: 'use strict'; var Grade; (function (Grade) { Grade[(Grade['A'] = 90)] = 'A'; Grade[(Grade['B'] = 80)] = 'B'; Grade[(Grade['C'] = 70)] = 'C'; Grade['F'] = 'fail'; })(Grade || (Grade = {})); Therefore, mapping values to keys works for numeric enum members, but not for string enum members: enum Grade { A = 90, B = 80, C = 70, F = 'fail', } const myGrade = Grade.A; console.log(Grade[myGrade]); // A console.log(Grade); // A const failGrade = Grade.F; console.log(failGrade); // fail console.log(Grade[failGrade]); // Element implicitly has an 'any' type because index expression is not of type 'number'. Ambient enums An ambient enum in TypeScript is a type of Enum that is defined in a declaration file (*.d.ts) without an associated implementation. It allows you to define a set of named constants that can be used in a type-safe way across different files without having to import the implementation details in each file. Computed and constant members In TypeScript, a computed member is a member of an Enum that has a value calculated at runtime, while a constant member is a member whose value is set at compile-time and cannot be changed during runtime. Computed members are allowed in regular Enums, while constant members are allowed in both regular and const enums. // Constant members enum Color { Red = 1, Green = 5, Blue = Red + Green, } console.log(Color.Blue); // 6 generation at compilation time // Computed members enum Color { Red = 1, Green = Math.pow(2, 2), Blue = Math.floor(Math.random() * 3) + 1, } console.log(Color.Blue); // random number generated at run time Enums are denoted by unions comprising their member types. The values of each member can be determined through constant or non- constant expressions, with members possessing constant values being assigned literal types. To illustrate, consider the declaration of type E and its subtypes E.A, E.B, and E.C. In this case, E represents the union E.A | E.B | E.C. const identity = (value: number) => value; enum E { A = 2 * 5, // Numeric literal B = 'bar', // String literal C = identity(42), // Opaque computed } console.log(E.C); //42 Narrowing TypeScript narrowing is the process of refining the type of a variable within a conditional block. This is useful when working with union types, where a variable can have more than one type. TypeScript recognizes several ways to narrow the type: typeof type guards The typeof type guard is one specific type guard in TypeScript that checks the type of a variable based on its built-in JavaScript type. const fn = (x: number | string) => { if (typeof x === 'number') { return x + 1; // x is number } return -1; }; Truthiness narrowing Truthiness narrowing in TypeScript works by checking whether a variable is truthy or falsy to narrow its type accordingly. const toUpperCase = (name: string | null) => { if (name) { return name.toUpperCase(); } else { return null; } }; Equality narrowing Equality narrowing in TypeScript works by checking whether a variable is equal to a specific value or not, to narrow its type accordingly. It is used in conjunction with switch statements and equality operators such as ===, !==, ==, and != to narrow down types. const checkStatus = (status: 'success' | 'error') => { switch (status) { case 'success': return true; case 'error': return null; } }; In Operator narrowing The in Operator narrowing in TypeScript is a way to narrow the type of a variable based on whether a property exists within the variable’s type. type Dog = { name: string; breed: string; }; type Cat = { name: string; likesCream: boolean; }; const getAnimalType = (pet: Dog | Cat) => { if ('breed' in pet) { return 'dog'; } else { return 'cat'; } }; instanceof narrowing The instanceof operator narrowing in TypeScript is a way to narrow the type of a variable based on its constructor function, by checking if an object is an instance of a certain class or interface. class Square { constructor(public width: number) {} } class Rectangle { constructor( public width: number, public height: number ) {} } function area(shape: Square | Rectangle) { if (shape instanceof Square) { return shape.width * shape.width; } else { return shape.width * shape.height; } } const square = new Square(5); const rectangle = new Rectangle(5, 10); console.log(area(square)); // 25 console.log(area(rectangle)); // 50 Assignments TypeScript narrowing using assignments is a way to narrow the type of a variable based on the value assigned to it. When a variable is assigned a value, TypeScript infers its type based on the assigned value, and it narrows the type of the variable to match the inferred type. let value: string | number; value = 'hello'; if (typeof value === 'string') { console.log(value.toUpperCase()); } value = 42; if (typeof value === 'number') { console.log(value.toFixed(2)); } Control Flow Analysis Control Flow Analysis in TypeScript is a way to statically analyze the code flow to infer the types of variables, allowing the compiler to narrow the types of those variables as needed, based on the results of the analysis. Prior to TypeScript 4.4, code flow analysis would only be applied to code within an if statement, but from TypeScript 4.4, it can also be applied to conditional expressions and discriminant property accesses indirectly referenced through const variables. For example: const f1 = (x: unknown) => { const isString = typeof x === 'string'; if (isString) { x.length; } }; const f2 = ( obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number } ) => { const isFoo = obj.kind === 'foo'; if (isFoo) { obj.foo; } else { obj.bar; } }; Some examples where narrowing does not occur: const f1 = (x: unknown) => { let isString = typeof x === 'string'; if (isString) { x.length; // Error, no narrowing because isString it is not const } }; const f6 = ( obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number } ) => { const isFoo = obj.kind === 'foo'; obj = obj; if (isFoo) { obj.foo; // Error, no narrowing because obj is assigned in function body } }; Notes: Up to five levels of indirection are analyzed in conditional expressions. Type Predicates Type Predicates in TypeScript are functions that return a boolean value and are used to narrow the type of a variable to a more specific type. const isString = (value: unknown): value is string => typeof value === 'string'; const foo = (bar: unknown) => { if (isString(bar)) { console.log(bar.toUpperCase()); } else { console.log('not a string'); } }; Discriminated Unions Discriminated Unions in TypeScript are a type of union type that uses a common property, known as the discriminant, to narrow down the set of possible types for the union. type Square = { kind: 'square'; // Discriminant size: number; }; type Circle = { kind: 'circle'; // Discriminant radius: number; }; type Shape = Square | Circle; const area = (shape: Shape) => { switch (shape.kind) { case 'square': return Math.pow(shape.size, 2); case 'circle': return Math.PI * Math.pow(shape.radius, 2); } }; const square: Square = { kind: 'square', size: 5 }; const circle: Circle = { kind: 'circle', radius: 2 }; console.log(area(square)); // 25 console.log(area(circle)); // 12.566370614359172 The never Type When a variable is narrowed to a type that cannot contain any values, the TypeScript compiler will infer that the variable must be of the never type. This is because The never Type represents a value that can never be produced. const printValue = (val: string | number) => { if (typeof val === 'string') { console.log(val.toUpperCase()); } else if (typeof val === 'number') { console.log(val.toFixed(2)); } else { // val has type never here because it can never be anything other than a string or a number const neverVal: never = val; console.log(`Unexpected value: ${neverVal}`); } }; Exhaustiveness checking Exhaustiveness checking is a feature in TypeScript that ensures all possible cases of a discriminated union are handled in a switch statement or an if statement. type Direction = 'up' | 'down'; const move = (direction: Direction) => { switch (direction) { case 'up': console.log('Moving up'); break; case 'down': console.log('Moving down'); break; default: const exhaustiveCheck: never = direction; console.log(exhaustiveCheck); // This line will never be executed } }; The never type is used to ensure that the default case is exhaustive and that TypeScript will raise an error if a new value is added to the Direction type without being handled in the switch statement. Object Types In TypeScript, object types describe the shape of an object. They specify the names and types of the object’s properties, as well as whether those properties are required or optional. In TypeScript, you can define object types in two primary ways: Interface which defines the shape of an object by specifying the names, types, and optionality of its properties. interface User { name: string; age: number; email?: string; } Type alias, similar to an interface, defines the shape of an object. However, it can also create a new custom type that is based on an existing type or a combination of existing types. This includes defining union types, intersection types, and other complex types. type Point = { x: number; y: number; }; It also possible to define a type anonymously: const sum = (x: { a: number; b: number }) => x.a + x.b; console.log(sum({ a: 5, b: 1 })); Tuple Type (Anonymous) A Tuple Type is a type that represents an array with a fixed number of elements and their corresponding types. A tuple type enforces a specific number of elements and their respective types in a fixed order. Tuple types are useful when you want to represent a collection of values with specific types, where the position of each element in the array has a specific meaning. type Point = [number, number]; Named Tuple Type (Labeled) Tuple types can include optional labels or names for each element. These labels are for readability and tooling assistance, and do not affect the operations you can perform with them. type T = string; type Tuple1 = [T, T]; type Tuple2 = [a: T, b: T]; type Tuple3 = [a: T, T]; // Named Tuple plus Anonymous Tuple Fixed Length Tuple A Fixed Length Tuple is a specific type of tuple that enforces a fixed number of elements of specific types, and disallows any modifications to the length of the tuple once it is defined. Fixed Length Tuples are useful when you need to represent a collection of values with a specific number of elements and specific types, and you want to ensure that the length and types of the tuple cannot be changed inadvertently. const x = [10, 'hello'] as const; x.push(2); // Error Union Type A Union Type is a type that represents a value that can be one of several types. Union Types are denoted using the | symbol between each possible type. let x: string | number; x = 'hello'; // Valid x = 123; // Valid Intersection Types An Intersection Type is a type that represents a value that has all the properties of two or more types. Intersection Types are denoted using the & symbol between each type. type X = { a: string; }; type Y = { b: string; }; type J = X & Y; // Intersection const j: J = { a: 'a', b: 'b', }; Type Indexing Type indexing refers to the ability to define types that can be indexed by a key that is not known in advance, using an index signature to specify the type for properties that are not explicitly declared. type Dictionary = { [key: string]: T; }; const myDict: Dictionary = { a: 'a', b: 'b' }; console.log(myDict['a']); // Returns a Type from Value Type from Value in TypeScript refers to the automatic inference of a type from a value or expression through type inference. const x = 'x'; // TypeScript can automatically infer that the type of the message variable is string Type from Func Return Type from Func Return refers to the ability to automatically infer the return type of a function based on its implementation. This allows TypeScript to determine the type of the value returned by the function without explicit type annotations. const add = (x: number, y: number) => x + y; // TypeScript can infer that the return type of the function is a number Type from Module Type from Module refers to the ability to use a module’s exported values to automatically infer their types. When a module exports a value with a specific type, TypeScript can use that information to automatically infer the type of that value when it is imported into another module. // calc.ts export const add = (x: number, y: number) => x + y; // index.ts import { add } from 'calc'; const r = add(1, 2); // r is number Mapped Types Mapped Types in TypeScript allow you to create new types based on an existing type by transforming each property using a mapping function. By mapping existing types, you can create new types that represent the same information in a different format. To create a mapped type, you access the properties of an existing type using the keyof operator and then alter them to produce a new type. In the following example: type MyMappedType = { [P in keyof T]: T[P][]; }; type MyType = { foo: string; bar: number; }; type MyNewType = MyMappedType; const x: MyNewType = { foo: ['hello', 'world'], bar: [1, 2, 3], }; we define MyMappedType to map over T’s properties, creating a new type with each property as an array of its original type. Using this, we create MyNewType to represent the same info as MyType, but with each property as an array. Mapped Type Modifiers Mapped Type Modifiers in TypeScript enable the transformation of properties within an existing type: readonly or +readonly: This renders a property in the mapped type as read-only. -readonly: This allows a property in the mapped type to be mutable. ?: This designates a property in the mapped type as optional. Examples: type ReadOnly = { readonly [P in keyof T]: T[P] }; // All properties marked as read-only type Mutable = { -readonly [P in keyof T]: T[P] }; // All properties marked as mutable type MyPartial = { [P in keyof T]?: T[P] }; // All properties marked as optional Conditional Types Conditional Types are a way to create a type that depends on a condition, where the type to be created is determined based on the result of the condition. They are defined using the extends keyword and a ternary operator to conditionally choose between two types. type IsArray = T extends any[] ? true : false; const myArray = [1, 2, 3]; const myNumber = 42; type IsMyArrayAnArray = IsArray; // Type true type IsMyNumberAnArray = IsArray; // Type false Distributive Conditional Types Distributive Conditional Types are a feature that allow a type to be distributed over a union of types, by applying a transformation to each member of the union individually. This can be especially useful when working with mapped types or higher-order types. type Nullable = T extends any ? T | null : never; type NumberOrBool = number | boolean; type NullableNumberOrBool = Nullable; // number | boolean | null infer Type Inference in Conditional Types The inferkeyword is used in conditional types to infer (extract) the type of a generic parameter from a type that depends on it. This allows you to write more flexible and reusable type definitions. type ElementType = T extends (infer U)[] ? U : never; type Numbers = ElementType; // number type Strings = ElementType; // string Predefined Conditional Types In TypeScript, Predefined Conditional Types are built-in conditional types provided by the language. They are designed to perform common type transformations based on the characteristics of a given type. Exclude: This type removes all the types from Type that are assignable to ExcludedType. Extract: This type extracts all the types from Union that are assignable to Type. NonNullable: This type removes null and undefined from Type. ReturnType: This type extracts the return type of a function Type. Parameters: This type extracts the parameter types of a function Type. Required: This type makes all properties in Type required. Partial: This type makes all properties in Type optional. Readonly: This type makes all properties in Type readonly. Template Union Types Template union types can be used to merge and manipulate text inside the type system for instance: type Status = 'active' | 'inactive'; type Products = 'p1' | 'p2'; type ProductId = `id-${Products}-${Status}`; // "id-p1- active" | "id-p1-inactive" | "id-p2-active" | "id-p2- inactive" Any type The any type is a special type (universal supertype) that can be used to represent any type of value (primitives, objects, arrays, functions, errors, symbols). It is often used in situations where the type of a value is not known at compile time, or when working with values from external APIs or libraries that do not have TypeScript typings. By utilizing any type, you are indicating to the TypeScript compiler that values should be represented without any limitations. In order to maximizing type safety in your code consider the following: Limit the usage of any to specific cases where the type is truly unknown. Do not return any types from a function as you will lose type safety in the code using that function weakening your type safety. Instead of any use @ts-ignore if you need to silence the compiler. let value: any; value = true; // Valid value = 7; // Valid Unknown type In TypeScript, the unknown type represents a value that is of an unknown type. Unlike any type, which allows for any type of value, unknown requires a type check or assertion before it can be used in a specific way so no operations are permitted on an unknown without first asserting or narrowing to a more specific type. The unknown type is only assignable to any type and the unknown type itself, it is a type-safe alternative to any. let value: unknown; let value1: unknown = value; // Valid let value2: any = value; // Valid let value3: boolean = value; // Invalid let value4: number = value; // Invalid const add = (a: unknown, b: unknown): number | undefined => typeof a === 'number' && typeof b === 'number' ? a + b : undefined; console.log(add(1, 2)); // 3 console.log(add('x', 2)); // undefined Void type The void type is used to indicate that a function does not return a value. const sayHello = (): void => { console.log('Hello!'); }; Never type The never type represents values that never occur. It is used to denote functions or expressions that never return or throw an error. For instance an infinite loop: const infiniteLoop = (): never => { while (true) { // do something } }; Throwing an error: const throwError = (message: string): never => { throw new Error(message); }; The never type is useful in ensuring type safety and catching potential errors in your code. It helps TypeScript analyze and infer more precise types when used in combination with other types and control flow statements, for instance: type Direction = 'up' | 'down'; const move = (direction: Direction): void => { switch (direction) { case 'up': // move up break; case 'down': // move down break; default: const exhaustiveCheck: never = direction; throw new Error(`Unhandled direction: ${exhaustiveCheck}`); } }; Interface and Type Common Syntax In TypeScript, interfaces define the structure of objects, specifying the names and types of properties or methods that an object must have. The common syntax for defining an interface in TypeScript is as follows: interface InterfaceName { property1: Type1; //... method1(arg1: ArgType1, arg2: ArgType2): ReturnType; //... } Similarly for type definition: type TypeName = { property1: Type1; //... method1(arg1: ArgType1, arg2: ArgType2): ReturnType; //... }; interface InterfaceName or type TypeName: Defines the name of the interface. property1: Type1: Specifies the properties of the interface along with their corresponding types. Multiple properties can be defined, each separated by a semicolon. method1(arg1: ArgType1, arg2: ArgType2): ReturnType;: Specifies the methods of the interface. Methods are defined with their names, followed by a parameter list in parentheses and the return type. Multiple methods can be defined, each separated by a semicolon. Example interface: interface Person { name: string; age: number; greet(): void; } Example of type: type TypeName = { property1: string; method1(arg1: string, arg2: string): string; }; In TypeScript, types are used to define the shape of data and enforce type checking. There are several common syntaxes for defining types in TypeScript, depending on the specific use case. Here are some examples: Basic Types let myNumber: number = 123; // number type let myBoolean: boolean = true; // boolean type let myArray: string[] = ['a', 'b']; // array of strings let myTuple: [string, number] = ['a', 123]; // tuple Objects and Interfaces const x: { name: string; age: number } = { name: 'Simon', age: 7 }; Union and Intersection Types type MyType = string | number; // Union type let myUnion: MyType = 'hello'; // Can be a string myUnion = 123; // Or a number type TypeA = { name: string }; type TypeB = { age: number }; type CombinedType = TypeA & TypeB; // Intersection type let myCombined: CombinedType = { name: 'John', age: 25 }; // Object with both name and age properties Built-in Type Primitives TypeScript has several built-in type primitives that can be used to define variables, function parameters, and return types: number: Represents numeric values, including integers and floating-point numbers. string: Represents textual data boolean: Represents logical values, which can be either true or false. null: Represents the absence of a value. undefined: Represents a value that has not been assigned or has not been defined. symbol: Represents a unique identifier. Symbols are typically used as keys for object properties. bigint: Represents arbitrary-precision integers. any: Represents a dynamic or unknown type. Variables of type any can hold values of any type, and they bypass type checking. void: Represents the absence of any type. It is commonly used as the return type of functions that do not return a value. never: Represents a type for values that never occur. It is typically used as the return type of functions that throw an error or enter an infinite loop. Common Built-in JS Objects TypeScript is a superset of JavaScript, it includes all the commonly used built-in JavaScript objects. You can find an extensive list of these objects on the Mozilla Developer Network (MDN) documentation website: https://developer.mozilla.org/en- US/docs/Web/JavaScript/Reference/Global_Objects Here is a list of some commonly used built-in JavaScript objects: Function Object Boolean Error Number BigInt Math Date String RegExp Array Map Set Promise Intl Overloads Function overloads in TypeScript allow you to define multiple function signatures for a single function name, enabling you to define functions that can be called in multiple ways. Here’s an example: // Overloads function sayHi(name: string): string; function sayHi(names: string[]): string[]; // Implementation function sayHi(name: unknown): unknown { if (typeof name === 'string') { return `Hi, ${name}!`; } else if (Array.isArray(name)) { return name.map(name => `Hi, ${name}!`); } throw new Error('Invalid value'); } sayHi('xx'); // Valid sayHi(['aa', 'bb']); // Valid Here’s another example of using function overloads within a class: class Greeter { message: string; constructor(message: string) { this.message = message; } // overload sayHi(name: string): string; sayHi(names: string[]): ReadonlyArray; // implementation sayHi(name: unknown): unknown { if (typeof name === 'string') { return `${this.message}, ${name}!`; } else if (Array.isArray(name)) { return name.map(name => `${this.message}, ${name}!`); } throw new Error('value is invalid'); } } console.log(new Greeter('Hello').sayHi('Simon')); Merging and Extension Merging and extension refer to two different concepts related to working with types and interfaces. Merging allows you to combine multiple declarations of the same name into a single definition, for example, when you define an interface with the same name multiple times: interface X { a: string; } interface X { b: number; } const person: X = { a: 'a', b: 7, }; Extension refers to the ability to extend or inherit from existing types or interfaces to create new ones. It is a mechanism to add additional properties or methods to an existing type without modifying its original definition. Example: interface Animal { name: string; eat(): void; } interface Bird extends Animal { sing(): void; } const dog: Bird = { name: 'Bird 1', eat() { console.log('Eating'); }, sing() { console.log('Singing'); }, }; Differences between Type and Interface Declaration merging (augmentation): Interfaces support declaration merging, which means that you can define multiple interfaces with the same name, and TypeScript will merge them into a single interface with the combined properties and methods. On the other hand, types do not support declaration merging. This can be helpful when you want to add extra functionality or customize existing types without modifying the original definitions or patching missing or incorrect types. interface A { x: string; } interface A { y: string; } const j: A = { x: 'xx', y: 'yy', }; Extending other types/interfaces: Both types and interfaces can extend other types/interfaces, but the syntax is different. With interfaces, you use the extends keyword to inherit properties and methods from other interfaces. However, an interface cannot extend a complex type like a union type. interface A { x: string; y: number; } interface B extends A { z: string; } const car: B = { x: 'x', y: 123, z: 'z', }; For types, you use the & operator to combine multiple types into a single type (intersection). interface A { x: string; y: number; } type B = A & { j: string; }; const c: B = { x: 'x', y: 123, j: 'j', }; Union and Intersection Types: Types are more flexible when it comes to defining Union and Intersection Types. With the type keyword, you can easily create union types using the | operator and intersection types using the & operator. While interfaces can also represent union types indirectly, they don’t have built-in support for intersection types. type Department = 'dep-x' | 'dep-y'; // Union type Person = { name: string; age: number; }; type Employee = { id: number; department: Department; }; type EmployeeInfo = Person & Employee; // Intersection Example with interfaces: interface A { x: 'x'; } interface B { y: 'y'; } type C = A | B; // Union of interfaces Class Class Common Syntax The class keyword is used in TypeScript to define a class. Below, you can see an example: class Person { private name: string; private age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } public sayHi(): void { console.log( `Hello, my name is ${this.name} and I am ${this.age} years old.` ); } } The class keyword is used to define a class named “Person”. The class has two private properties: name of type string and age of type number. The constructor is defined using the constructor keyword. It takes name and age as parameters and assigns them to the corresponding properties. The class has a public method named sayHi that logs a greeting message. To create an instance of a class in TypeScript, you can use the new keyword followed by the class name, followed by parentheses (). For instance: const myObject = new Person('John Doe', 25); myObject.sayHi(); // Output: Hello, my name is John Doe and I am 25 years old. Constructor Constructors are special methods within a class that are used to initialize the object’s properties when an instance of the class is created. class Person { public name: string; public age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log( `Hello, my name is ${this.name} and I'm ${this.age} years old.` ); } } const john = new Person('Simon', 17); john.sayHello(); It is possible to overload a constructor using the following syntax: type Sex = 'm' | 'f'; class Person { name: string; age: number; sex: Sex; constructor(name: string, age: number, sex?: Sex); constructor(name: string, age: number, sex: Sex) { this.name = name; this.age = age; this.sex = sex ?? 'm'; } } const p1 = new Person('Simon', 17); const p2 = new Person('Alice', 22, 'f'); In TypeScript, it is possible to define multiple constructor overloads, but you can have only one implementation that must be compatible with all the overloads, this can be achieved by using an optional parameter. class Person { name: string; age: number; constructor(); constructor(name: string); constructor(name: string, age: number); constructor(name?: string, age?: number) { this.name = name ?? 'Unknown'; this.age = age ?? 0; } displayInfo() { console.log(`Name: ${this.name}, Age: ${this.age}`); } } const person1 = new Person(); person1.displayInfo(); // Name: Unknown, Age: 0 const person2 = new Person('John'); person2.displayInfo(); // Name: John, Age: 0 const person3 = new Person('Jane', 25); person3.displayInfo(); // Name: Jane, Age: 25 Private and Protected Constructors In TypeScript, constructors can be marked as private or protected, which restricts their accessibility and usage. Private Constructors: Can be called only within the class itself. Private constructors are often used in scenarios where you want to enforce a singleton pattern or restrict the creation of instances to a factory method within the class Protected Constructors: Protected constructors are useful when you want to create a base class that should not be instantiated directly but can be extended by subclasses. class BaseClass { protected constructor() {} } class DerivedClass extends BaseClass { private value: number; constructor(value: number) { super(); this.value = value; } } // Attempting to instantiate the base class directly will result in an error // const baseObj = new BaseClass(); // Error: Constructor of class 'BaseClass' is protected. // Create an instance of the derived class const derivedObj = new DerivedClass(10); Access Modifiers Access Modifiers private, protected, and public are used to control the visibility and accessibility of class members, such as properties and methods, in TypeScript classes. These modifiers are essential for enforcing encapsulation and establishing boundaries for accessing and modifying the internal state of a class. The private modifier restricts access to the class member only within the containing class. The protected modifier allows access to the class member within the containing class and its derived classes. The public modifier provides unrestricted access to the class member, allowing it to be accessed from anywhere.” Get & Set Getters and setters are special methods that allow you to define custom access and modification behavior for class properties. They enable you to encapsulate the internal state of an object and provide additional logic when getting or setting the values of properties. In TypeScript, getters and setters are defined using the get and set keywords respectively. Here’s an example: class MyClass { private _myProperty: string; constructor(value: string) { this._myProperty = value; } get myProperty(): string { return this._myProperty; } set myProperty(value: string) { this._myProperty = value; } } Auto-Accessors in Classes TypeScript version 4.9 adds support for auto-accessors, a forthcoming ECMAScript feature. They resemble class properties but are declared with the “accessor” keyword. class Animal { accessor name: string; constructor(name: string) { this.name = name; } } Auto-accessors are “de-sugared” into private get and set accessors, operating on an inaccessible property. class Animal { #__name: string; get name() { return this.#__name; } set name(value: string) { this.#__name = name; } constructor(name: string) { this.name = name; } } this In TypeScript, the this keyword refers to the current instance of a class within its methods or constructors. It allows you to access and modify the properties and methods of the class from within its own scope. It provides a way to access and manipulate the internal state of an object within its own methods. class Person { private name: string; constructor(name: string) { this.name = name; } public introduce(): void { console.log(`Hello, my name is ${this.name}.`); } } const person1 = new Person('Alice'); person1.introduce(); // Hello, my name is Alice. Parameter Properties Parameter properties allow you to declare and initialize class properties directly within the constructor parameters avoiding boilerplate code, example: class Person { constructor( private name: string, public age: number ) { // The "private" and "public" keywords in the constructor // automatically declare and initialize the corresponding class properties. } public introduce(): void { console.log( `Hello, my name is ${this.name} and I am ${this.age} years old.` ); } } const person = new Person('Alice', 25); person.introduce(); Abstract Classes Abstract Classes are used in TypeScript mainly for inheritance, they provide a way to define common properties and methods that can be inherited by subclasses. This is useful when you want to define common behavior and enforce that subclasses implement certain methods. They provide a way to create a hierarchy of classes where the abstract base class provides a shared interface and common functionality for the subclasses. abstract class Animal { protected name: string; constructor(name: string) { this.name = name; } abstract makeSound(): void; } class Cat extends Animal { makeSound(): void { console.log(`${this.name} meows.`); } } const cat = new Cat('Whiskers'); cat.makeSound(); // Output: Whiskers meows. With Generics Classes with generics allow you to define reusable classes which can work with different types. class Container { private item: T; constructor(item: T) { this.item = item; } getItem(): T { return this.item; } setItem(item: T): void { this.item = item; } } const container1 = new Container(42); console.log(container1.getItem()); // 42 const container2 = new Container('Hello'); container2.setItem('World'); console.log(container2.getItem()); // World Decorators Decorators provide a mechanism to add metadata, modify behavior, validate, or extend the functionality of the target element. They are functions that execute at runtime. Multiple decorators can be applied to a declaration. Decorators are experimental features, and the following examples are only compatible with TypeScript version 5 or above using ES6. For TypeScript versions prior to 5, they should be enabled using the experimentalDecorators property in your tsconfig.json or by using --experimentalDecorators in your command line (but the following example won’t work). Some of the common use cases for decorators include: Watching property changes. Watching method calls. Adding extra properties or methods. Runtime validation. Automatic serialization and deserialization. Logging. Authorization and authentication. Error guarding. Note: Decorators for version 5 do not allow decorating parameters. Types of decorators: Class Decorators Class Decorators are useful for extending an existing class, such as adding properties or methods, or collecting instances of a class. In the following example, we add a toString method that converts the class into a string representation. type Constructor = new (...args: any[]) => T; function toString( Value: Class, context: ClassDecoratorContext ) { return class extends Value { constructor(...args: any[]) { super(...args); console.log(JSON.stringify(this)); console.log(JSON.stringify(context)); } }; } @toString class Person { name: string; constructor(name: string) { this.name = name; } greet() { return 'Hello, ' + this.name; } } const person = new Person('Simon'); Property Decorator Property decorators are useful for modifying the behavior of a property, such as changing the initialization values. In the following code, we have a script that sets a property to always be in uppercase: function upperCase( target: undefined, context: ClassFieldDecoratorContext ) { return function (this: T, value: string) { return value.toUpperCase(); }; } class MyClass { @upperCase prop1 = 'hello!'; } console.log(new MyClass().prop1); // Logs: HELLO! Method Decorator Method decorators allow you to change or enhance the behavior of methods. Below is an example of a simple logger: function log( target: (this: This,...args: Args) => Return, context: ClassMethodDecoratorContext< This, (this: This,...args: Args) => Return > ) { const methodName = String(context.name); function replacementMethod(this: This,...args: Args): Return { console.log(`LOG: Entering method '${methodName}'.`); const result = target.call(this,...args); console.log(`LOG: Exiting method '${methodName}'.`); return result; } return replacementMethod; } class MyClass { @log sayHello() { console.log('Hello!'); } } new MyClass().sayHello(); It logs: LOG: Entering method 'sayHello'. Hello! LOG: Exiting method 'sayHello'. Getter and Setter Decorators Getter and setter decorators allow you to change or enhance the behavior of class accessors. They are useful, for instance, for validating property assignments. Here’s a simple example for a getter decorator: function range(min: number, max: number) { return function ( target: (this: This) => Return, context: ClassGetterDecoratorContext ) { return function (this: This): Return { const value = target.call(this); if (value < min || value > max) { throw 'Invalid'; } Object.defineProperty(this, context.name, { value, enumerable: true, }); return value; }; }; } class MyClass { private _value = 0; constructor(value: number) { this._value = value; } @range(1, 100) get getValue(): number { return this._value; } } const obj = new MyClass(10); console.log(obj.getValue); // Valid: 10 const obj2 = new MyClass(999); console.log(obj2.getValue); // Throw: Invalid! Decorator Metadata Decorator Metadata simplifies the process for decorators to apply and utilize metadata in any class. They can access a new metadata property on the context object, which can serve as a key for both primitives and objects. Metadata information can be accessed on the class via Symbol.metadata. Metadata can be used for various purposes, such as debugging, serialization, or dependency injection with decorators. //@ts-ignore Symbol.metadata ??= Symbol('Symbol.metadata'); // Simple polify type Context = | ClassFieldDecoratorContext | ClassAccessorDecoratorContext | ClassMethodDecoratorContext; // Context contains property metadata: DecoratorMetadata function setMetadata(_target: any, context: Context) { // Set the metadata object with a primitive value context.metadata[context.name] = true; } class MyClass { @setMetadata a = 123; @setMetadata accessor b = 'b'; @setMetadata fn() {} } const metadata = MyClass[Symbol.metadata]; // Get metadata information console.log(JSON.stringify(metadata)); // {"bar":true,"baz":true,"foo":true} Inheritance Inheritance refers to the mechanism by which a class can inherit properties and methods from another class, known as the base class or superclass. The derived class, also called the child class or subclass, can extend and specialize the functionality of the base class by adding new properties and methods or overriding existing ones. class Animal { name: string; constructor(name: string) { this.name = name; } speak(): void { console.log('The animal makes a sound'); } } class Dog extends Animal { breed: string; constructor(name: string, breed: string) { super(name); this.breed = breed; } speak(): void { console.log('Woof! Woof!'); } } // Create an instance of the base class const animal = new Animal('Generic Animal'); animal.speak(); // The animal makes a sound // Create an instance of the derived class const dog = new Dog('Max', 'Labrador'); dog.speak(); // Woof! Woof!" TypeScript does not support multiple inheritance in the traditional sense and instead allows inheritance from a single base class. TypeScript supports multiple interfaces. An interface can define a contract for the structure of an object, and a class can implement multiple interfaces. This allows a class to inherit behavior and structure from multiple sources. interface Flyable { fly(): void; } interface Swimmable { swim(): void; } class FlyingFish implements Flyable, Swimmable { fly() { console.log('Flying...'); } swim() { console.log('Swimming...'); } } const flyingFish = new FlyingFish(); flyingFish.fly(); flyingFish.swim(); The class keyword in TypeScript, similar to JavaScript, is often referred to as syntactic sugar. It was introduced in ECMAScript 2015 (ES6) to offer a more familiar syntax for creating and working with objects in a class-based manner. However, it’s important to note that TypeScript, being a superset of JavaScript, ultimately compiles down to JavaScript, which remains prototype-based at its core. Statics TypeScript has static members. To access the static members of a class, you can use the class name followed by a dot, without the need to create an object. class OfficeWorker { static memberCount: number = 0; constructor(private name: string) { OfficeWorker.memberCount++; } } const w1 = new OfficeWorker('James'); const w2 = new OfficeWorker('Simon'); const total = OfficeWorker.memberCount; console.log(total); // 2 Property initialization There are several ways how you can initialize properties for a class in TypeScript: Inline: In the following example these initial values will be used when an instance of the class is created. class MyClass { property1: string = 'default value'; property2: number = 42; } In the constructor: class MyClass { property1: string; property2: number; constructor() { this.property1 = 'default value'; this.property2 = 42; } } Using constructor parameters: class MyClass { constructor( private property1: string = 'default value', public property2: number = 42 ) { // There is no need to assign the values to the properties explicitly. } log() { console.log(this.property2); } } const x = new MyClass(); x.log(); Method overloading Method overloading allows a class to have multiple methods with the same name but different parameter types or a different number of parameters. This allows us to call a method in different ways based on the arguments passed. class MyClass { add(a: number, b: number): number; // Overload signature 1 add(a: string, b: string): string; // Overload signature 2 add(a: number | string, b: number | string): number | string { if (typeof a === 'number' && typeof b === 'number') { return a + b; } if (typeof a === 'string' && typeof b === 'string') { return a.concat(b); } throw new Error('Invalid arguments'); } } const r = new MyClass(); console.log(r.add(10, 5)); // Logs 15 Generics Generics allow you to create reusable components and functions that can work with multiple types. With generics, you can parameterize types, functions, and interfaces, allowing them to operate on different types without explicitly specifying them beforehand. Generics allow you to make code more flexible and reusable. Generic Type To define a generic type, you use angle brackets () to specify the type parameters, for instance: function identity(arg: T): T { return arg; } const a = identity('x'); const b = identity(123); const getLen = (data: ReadonlyArray) => data.length; const len = getLen([1, 2, 3]); Generic Classes Generics can be applied also to classes, in this way they can work with multiple types by using type parameters. This is useful to create reusable class definitions that can operate on different data types while maintaining type safety. class Container { private item: T; constructor(item: T) { this.item = item; } getItem(): T { return this.item; } } const numberContainer = new Container(123); console.log(numberContainer.getItem()); // 123 const stringContainer = new Container('hello'); console.log(stringContainer.getItem()); // hello Generic Constraints Generic parameters can be constrained using the extends keyword followed by a type or interface that the type parameter must satisfy. In the following example T it is must containing a properly length in order to be valid: const printLen = (value: T): void => { console.log(value.length); }; printLen('Hello'); // 5 printLen([1, 2, 3]); // 3 printLen({ length: 10 }); // 10 printLen(123); // Invalid An interesting feature of generic introduced in version 3.4 RC is Higher order function type inference which introduced propagated generic type arguments: declare function pipe( ab: (...args: A) => B, bc: (b: B) => C ): (...args: A) => C; declare function list(a: T): T[]; declare function box(x: V): { value: V }; const listBox = pipe(list, box); // (a: T) => { value: T[] } const boxList = pipe(box, list); // (x: V) => { value: V }[] This functionality allows more easily typed safe pointfree style programming which is common in functional programming. Generic contextual narrowing Contextual narrowing for generics is the mechanism in TypeScript that allows the compiler to narrow down the type of a generic parameter based on the context in which it is used, it is useful when working with generic types in conditional statements: function process(value: T): void { if (typeof value === 'string') { // Value is narrowed down to type 'string' console.log(value.length); } else if (typeof value === 'number') { // Value is narrowed down to type 'number' console.log(value.toFixed(2)); } } process('hello'); // 5 process(3.14159); // 3.14 Erased Structural Types In TypeScript, objects do not have to match a specific, exact type. For instance, if we create an object that fulfills an interface’s requirements, we can utilize that object in places where that interface is required, even if there was no explicit connection between them. Example: type NameProp1 = { prop1: string; }; function log(x: NameProp1) { console.log(x.prop1); } const obj = { prop2: 123, prop1: 'Origin', }; log(obj); // Valid Namespacing In TypeScript, namespaces are used to organize code into logical containers, preventing naming collisions and providing a way to group related code together. The usage of the export keywords allows access to the namespace in “outside” modules. export namespace MyNamespace { export interface MyInterface1 { prop1: boolean; } export interface MyInterface2 { prop2: string; } } const a: MyNamespace.MyInterface1 = { prop1: true, }; Symbols Symbols are a primitive data type that represents an immutable value which is guaranteed to be globally unique throughout the lifetime of the program. Symbols can be used as keys for object properties and provide a way to create non-enumerable properties. const key1: symbol = Symbol('key1'); const key2: symbol = Symbol('key2'); const obj = { [key1]: 'value 1', [key2]: 'value 2', }; console.log(obj[key1]); // value 1 console.log(obj[key2]); // value 2 In WeakMaps and WeakSets, symbols are now permissible as keys. Triple-Slash Directives Triple-slash directives are special comments that provide instructions to the compiler about how to process a file. These directives begin with three consecutive slashes (///) and are typically placed at the top of a TypeScript file and have no effects on the runtime behavior. Triple-slash directives are used to reference external dependencies, specify module loading behavior, enable/disable certain compiler features, and more. Few examples: Referencing a declaration file: /// Indicate the module format: /// Enable compiler options, in the following example strict mode: /// Type Manipulation Creating Types from Types Is it possible to create new types composing, manipulating or transforming existing types. Intersection Types (&): Allow you to combine multiple types into a single type: type A = { foo: number }; type B = { bar: string }; type C = A & B; // Intersection of A and B const obj: C = { foo: 42, bar: 'hello' }; Union Types (|): Allow you to define a type that can be one of several types: type Result = string | number; const value1: Result = 'hello'; const value2: Result = 42; Mapped Types: Allow you to transform the properties of an existing type to create new type: type Mutable = { readonly [P in keyof T]: T[P]; }; type Person = { name: string; age: number; }; type ImmutablePerson = Mutable; // Properties become read-only Conditional types: Allow you to create types based on some conditions: type ExtractParam = T extends (param: infer P) => any ? P : never; type MyFunction = (name: string) => number; type ParamType = ExtractParam; // string Indexed Access Types In TypeScript is it possible to access and manipulate the types of properties within another type using an index, Type[Key]. type Person = { name: string; age: number; }; type AgeType = Person['age']; // number type MyTuple = [string, number, boolean]; type MyType = MyTuple; // boolean Utility Types Several built-in utility types can be used to manipulate types, below a list of the most common used: Awaited Constructs a type recursively unwrap Promises. type A = Awaited; // string Partial Constructs a type with all properties of T set to optional. type Person = { name: string; age: number; }; type A = Partial; // { name?: string | undefined; age?: number | undefined; } Required Constructs a type with all properties of T set to required. type Person = { name?: string; age?: number; }; type A = Required; // { name: string; age: number; } Readonly Constructs a type with all properties of T set to readonly. type Person = { name: string; age: number; }; type A = Readonly; const a: A = { name: 'Simon', age: 17 }; a.name = 'John'; // Invalid Record Constructs a type with a set of properties K of type T. type Product = { name: string; price: number; }; const products: Record = { apple: { name: 'Apple', price: 0.5 }, banana: { name: 'Banana', price: 0.25 }, }; console.log(products.apple); // { name: 'Apple', price: 0.5 } Pick Constructs a type by picking the specified properties K from T. type Product = { name: string; price: number; }; type Price = Pick; // { price: number; } Omit Constructs a type by omitting the specified properties K from T. type Product = { name: string; price: number; }; type Name = Omit; // { name: string; } Exclude Constructs a type by excluding all values of type U from T. type Union = 'a' | 'b' | 'c'; type MyType = Exclude; // b Extract Constructs a type by extracting all values of type U from T. type Union = 'a' | 'b' | 'c'; type MyType = Extract; // a | c NonNullable Constructs a type by excluding null and undefined from T. type Union = 'a' | null | undefined | 'b'; type MyType = NonNullable; // 'a' | 'b' Parameters Extracts the parameter types of a function type T. type Func = (a: string, b: number) => void; type MyType = Parameters; // [a: string, b: number] ConstructorParameters Extracts the parameter types of a constructor function type T. class Person { constructor( public name: string, public age: number ) {} } type PersonConstructorParams = ConstructorParameters; // [name: string, age: number] const params: PersonConstructorParams = ['John', 30]; const person = new Person(...params); console.log(person); // Person { name: 'John', age: 30 } ReturnType Extracts the return type of a function type T. type Func = (name: string) => number; type MyType = ReturnType; // number InstanceType Extracts the instance type of a class type T. class Person { name: string; constructor(name: string) { this.name = name; } sayHello() { console.log(`Hello, my name is ${this.name}!`); } } type PersonInstance = InstanceType; const person: PersonInstance = new Person('John'); person.sayHello(); // Hello, my name is John! ThisParameterType Extracts the type of ‘this’ parameter from a function type T. interface Person { name: string; greet(this: Person): void; } type PersonThisType = ThisParameterType; // Person OmitThisParameter Removes the ‘this’ parameter from a function type T. function capitalize(this: String) { return this.toUpperCase + this.substring(1).toLowerCase(); } type CapitalizeType = OmitThisParameter; // () => string ThisType Servers as a market for a contextual this type. type Logger = { log: (error: string) => void; }; let helperFunctions: { [name: string]: Function } & ThisType = { hello: function () { this.log('some error'); // Valid as "log" is a part of "this". this.update(); // Invalid }, }; Uppercase Make uppercase the name of the input type T. type MyType = Uppercase; // "ABC" Lowercase Make lowercase the name of the input type T. type MyType = Lowercase; // "abc" Capitalize Capitalize the name of the input type T. type MyType = Capitalize; // "Abc" Uncapitalize Uncapitalize the name of the input type T. type MyType = Uncapitalize; // "abc" Others Errors and Exception Handling TypeScript allows you to catch and handle errors using standard JavaScript error handling mechanisms: Try-Catch-Finally Blocks: try { // Code that might throw an error } catch (error) { // Handle the error } finally { // Code that always executes, finally is optional } You can also handle different types of error: try { // Code that might throw different types of errors } catch (error) { if (error instanceof TypeError) { // Handle TypeError } else if (error instanceof RangeError) { // Handle RangeError } else { // Handle other errors } } Custom Error Types: It is possible to specify more specific error by extending on the Error class: class CustomError extends Error { constructor(message: string) { super(message); this.name = 'CustomError'; } } throw new CustomError('This is a custom error.'); Mixin classes Mixin classes allow you to combine and compose behavior from multiple classes into a single class. They provide a way to reuse and extend functionality without the need for deep inheritance chains. abstract class Identifiable { name: string = ''; logId() { console.log('id:', this.name); } } abstract class Selectable { selected: boolean = false; select() { this.selected = true; console.log('Select'); } deselect() { this.selected = false; console.log('Deselect'); } } class MyClass { constructor() {} } // Extend MyClass to include the behavior of Identifiable and Selectable interface MyClass extends Identifiable, Selectable {} // Function to apply mixins to a class function applyMixins(source: any, baseCtors: any[]) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { let descriptor = Object.getOwnPropertyDescriptor( baseCtor.prototype, name ); if (descriptor) { Object.defineProperty(source.prototype, name, descriptor); } }); }); } // Apply the mixins to MyClass applyMixins(MyClass, [Identifiable, Selectable]); let o = new MyClass(); o.name = 'abc'; o.logId(); o.select(); Asynchronous Language Features As TypeScript is a superset of JavaScript, it has built-in asynchronous language features of JavaScript as: Promises: Promises are a way to handle asynchronous operations and their results using methods like.then() and.catch() to handle success and error conditions. To learn more: https://developer.mozilla.org/en- US/docs/Web/JavaScript/Reference/Global_Objects/Promise Async/await: Async/await keywords are a way to provide a more synchronous- looking syntax for working with Promises. The async keyword is used to define an asynchronous function, and the await keyword is used within an async function to pause execution until a Promise is resolved or rejected. To learn more: https://developer.mozilla.org/en- US/docs/Web/JavaScript/Reference/Statements/async_function https://developer.mozilla.org/en- US/docs/Web/JavaScript/Reference/Operators/await The following API are well supported in TypeScript: Fetch API: https://developer.mozilla.org/en- US/docs/Web/API/Fetch_API Web Workers: https://developer.mozilla.org/en- US/docs/Web/API/Web_Workers_API Shared Workers: https://developer.mozilla.org/en- US/docs/Web/API/SharedWorker WebSocket: https://developer.mozilla.org/en- US/docs/Web/API/WebSockets_API Iterators and Generators Both Interators and Generators are well supported in TypeScript. Iterators are objects that implement the iterator protocol, providing a way to access elements of a collection or sequence one by one. It is a structure that contains a pointer to the next element in the iteration. They have a next() method that returns the next value in the sequence along with a boolean indicating if the sequence is done. class NumberIterator implements Iterable { private current: number; constructor( private start: number, private end: number ) { this.current = start; } public next(): IteratorResult { if (this.current { console.log(`Name is ${person!.name}`); }; Defaulted declarations Defaulted declarations are used when a variable or parameter is assigned a default value. This means that if no value is provided for that variable or parameter, the default value will be used instead. function greet(name: string = 'Anonymous'): void { console.log(`Hello, ${name}!`); } greet(); // Hello, Anonymous! greet('John'); // Hello, John! Optional Chaining The optional chaining operator ?. works like the regular dot operator (.) for accessing properties or methods. However, it gracefully handles null or undefined values by terminating the expression and returning undefined, instead of throwing an error. type Person = { name: string; age?: number; address?: { street?: string; city?: string; }; }; const person: Person = { name: 'John', }; console.log(person.address?.city); // undefined Nullish coalescing operator (??) The nullish coalescing operator ?? returns the right-hand side value if the left-hand side is null or undefined; otherwise, it returns the left-hand side value. const foo = null ?? 'foo'; console.log(foo); // foo const baz = 1 ?? 'baz'; const baz2 = 0 ?? 'baz'; console.log(baz); // 1 console.log(baz2); // 0 Template Literal Types Template Literal Types allow to manipulate string value at type level and generate new string types based on existing ones. They are useful to create more expressive and precise types from string-based operations. type Department = 'engineering' | 'hr'; type Language = 'english' | 'spanish'; type Id = `${Department}-${Language}-id`; // "engineering- english-id" | "engineering-spanish-id" | "hr-english-id" | "hr-spanish-id" Function overloading Function overloading allows you to define multiple function signatures for the same function name, each with different parameter types and return type. When you call an overloaded function, TypeScript uses the provided arguments to determine the correct function signature: function makeGreeting(name: string): string; function makeGreeting(names: string[]): string[]; function makeGreeting(person: unknown): unknown { if (typeof person === 'string') { return `Hi ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `Hi, ${name}!`); } throw new Error('Unable to greet'); } makeGreeting('Simon'); makeGreeting(['Simone', 'John']); Recursive Types A Recursive Type is a type that can refer to itself. This is useful for defining data structures that have a hierarchical or recursive structure (potentially infinite nesting), such as linked lists, trees, and graphs. type ListNode = { data: T; next: ListNode | undefined; }; Recursive Conditional Types It is possible to define complex type relationships using logic and recursion in TypeScript. Let’s break it down in simple terms: Conditional Types: allows you to define types based on boolean conditions: type CheckNumber = T extends number ? 'Number' : 'Not a number'; type A = CheckNumber; // 'Number' type B = CheckNumber; // 'Not a number' Recursion: means a type definition that refers to itself within its own definition: type Json = string | number | boolean | null | Json[] | { [key: string]: Json }; const data: Json = { prop1: true, prop2: 'prop2', prop3: { prop4: [], }, }; Recursive Conditional Types combine both conditional logic and recursion. It means that a type definition can depend on itself through conditional logic, creating complex and flexible type relationships. type Flatten = T extends Array ? Flatten : T; type NestedArray = [1, [2, [3, 4], 5], 6]; type FlattenedArray = Flatten; // 2 | 3 | 4 | 5 | 1 | 6 ECMAScript Module Support in Node.js Node.js added support for ECMAScript Modules starting from version 15.3.0, and TypeScript has had ECMAScript Module Support for Node.js since version 4.7. This support can be enabled by using the module property with the value nodenext in the tsconfig.json file. Here’s an example: { "compilerOptions": { "module": "nodenext", "outDir": "./lib", "declaration": true } } Node.js supports two file extensions for modules:.mjs for ES modules and.cjs for CommonJS modules. The equivalent file extensions in TypeScript are.mts for ES modules and.cts for CommonJS modules. When the TypeScript compiler transpiles these files to JavaScript, it will create.mjs and.cjs files. If you want to use ES modules in your project, you can set the type property to “module” in your package.json file. This instructs Node.js to treat the project as an ES module project. Additionally, TypeScript also supports type declarations in.d.ts files. These declaration files provide type information for libraries or modules written in TypeScript, allowing other developers to utilize them with TypeScript’s type checking and auto-completion features. Assertion Functions In TypeScript, assertion functions are functions that indicate the verification of a specific condition based on their return value. In their simplest form, an assert function examines a provided predicate and raises an error when the predicate evaluates to false. function isNumber(value: unknown): asserts value is number { if (typeof value !== 'number') { throw new Error('Not a number'); } } Or can be declared as function expression: type AssertIsNumber = (value: unknown) => asserts value is number; const isNumber: AssertIsNumber = value => { if (typeof value !== 'number') { throw new Error('Not a number'); } }; Assertion functions share similarities with type guards. Type guards were initially introduced to perform runtime checks and ensure the type of a value within a specific scope. Specifically, a type guard is a function that evaluates a type predicate and returns a boolean value indicating whether the predicate is true or false. This differs slightly from assertion functions,where the intention is to throw an error rather than returning false when the predicate is not satisfied. Example of type guard: const isNumber = (value: unknown): value is number => typeof value === 'number'; Variadic Tuple Types Variadic Tuple Types are a features introduces in TypeScript version 4.0, let’s start to learn them by revise what is a tuple: A tuple type is an array which has a defined length, and were the type of each element is known: type Student = [string, number]; const [name, age]: Student = ['Simone', 20]; The term “variadic” means indefinite arity (accept a variable number of arguments). A variadic tuple is a tuple type which has all the property as before but the exact shape is not defined yet: type Bar = [boolean,...T, number]; type A = Bar; // [boolean, boolean, number] type B = Bar; // [boolean, 'a', 'b', number] type C = Bar; // [boolean, number] In the previous code we can see that the tuple shape is defined by the T generic passed in. Variadic tuples can accept multiple generics make them very flexible: type Bar = [...T, boolean,...G]; type A = Bar; // [number, boolean, string] type B = Bar; // ["a", "b", boolean, boolean] With the new variadic tuples we can use: The spreads in tuple type syntax can now be generic, so we can represent higher-order operation on tuples and arrays even when we do not know the actual types we are operating over. The rest elements can occur anywhere in a tuple. Example: type Items = readonly unknown[]; function concat( arr1: T, arr2: U ): [...T,...U] { return [...arr1,...arr2]; } concat([1, 2, 3], ['4', '5', '6']); // [1, 2, 3, "4", "5", "6"] Boxed types Boxed types refer to the wrapper objects that are used to represent primitive types as objects. These wrapper objects provide additional functionality and methods that are not available directly on the primitive values. When you access a method like charAt or normalize on a string primitive, JavaScript wraps it in a String object, calls the method, and then throws the object away. Demonstration: const originalNormalize = String.prototype.normalize; String.prototype.normalize = function () { console.log(this, typeof this); return originalNormalize.call(this); }; console.log('\u0041'.normalize()); TypeScript represents this differentiation by providing separate types for the primitives and their corresponding object wrappers: string => String number => Number boolean => Boolean symbol => Symbol bigint => BigInt The boxed types are usually not needed. Avoid using boxed types and instead use type for the primitives, for instance string instead of String. Covariance and Contravariance in TypeScript Covariance and Contravariance are used to describe how relationships work when dealing with inheritance or assignment of types. Covariance means that a type relationship preserves the direction of inheritance or assignment, so if a type A is a subtype of type B, then an array of type A is also considered a subtype of an array of type B. The important thing to note here is that the subtype relationship is maintained this means that Covariance accept subtype but doesn’t accept supertype. Contravariance means that a type relationship reverses the direction of inheritance or assignment, so if a type A is a subtype of type B, then an array of type B is considered a subtype of an array of type A. The subtype relationship is reversed this means that Contravariance accept supertype but doesn’t accept subtype. Notes: Bivariance means accept both supertype & subtype. Example: Let’s say we have a space for all animals and a separate space just for dogs. In Covariance, you can put all the dogs in the animals space because dogs are a type of animal. But you cannot put all the animals in the dog space because there might be other animals mixed in. In Contravariance, you cannot put all the animals in the dogs space because the animals space might contain other animals as well. However, you can put all the dogs in the animal space because all dogs are also animals. // Covariance example class Animal { name: string; constructor(name: string) { this.name = name; } } class Dog extends Animal { breed: string; constructor(name: string, breed: string) { super(name); this.breed = breed; } } let animals: Animal[] = []; let dogs: Dog[] = []; // Covariance allows assigning subtype (Dog) array to supertype (Animal) array animals = dogs; dogs = animals; // Invalid: Type 'Animal[]' is not assignable to type 'Dog[]' // Contravariance example type Feed = (animal: T) => void; let feedAnimal: Feed = (animal: Animal) => { console.log(`Animal name: ${animal.name}`); }; let feedDog: Feed = (dog: Dog) => { console.log(`Dog name: ${dog.name}, Breed: ${dog.breed}`); }; // Contravariance allows assigning supertype (Animal) callback to subtype (Dog) callback feedDog = feedAnimal; feedAnimal = feedDog; // Invalid: Type 'Feed' is not assignable to type 'Feed'. In TypeScript, type relationships for arrays are covariant, while type relationships for function parameters are contravariant. This means that TypeScript exhibits both covariance and contravariance, depending on the context. Optional Variance Annotations for Type Parameters As of TypeScript 4.7.0, we can use the out and in keywords to be specific about Variance annotation. For Covariant, use the out keyword: type AnimalCallback = () => T; // T is Covariant here And for Contravariant, use the in keyword: type AnimalCallback = (value: T) => void; // T is Contravariance here Template String Pattern Index Signatures Template string pattern index signatures allow us to define flexible index signatures using template string patterns. This feature enables us to create objects that can be indexed with specific patterns of string keys, providing more control and specificity when accessing and manipulating properties. TypeScript from version 4.4 allows index signatures for symbols and template string patterns. const uniqueSymbol = Symbol('description'); type MyKeys = `key-${string}`; type MyObject = { [uniqueSymbol]: string; [key: MyKeys]: number; }; const obj: MyObject = { [uniqueSymbol]: 'Unique symbol key', 'key-a': 123, 'key-b': 456, }; console.log(obj[uniqueSymbol]); // Unique symbol key console.log(obj['key-a']); // 123 console.log(obj['key-b']); // 456 The satisfies Operator The satisfies allows you to check if a given type satisfies a specific interface or condition. In other words, it ensures that a type has all the required properties and methods of a specific interface. It is a way to ensure a variable fits into a definition of a type Here is an example: type Columns = 'name' | 'nickName' | 'attributes'; type User = Record; // Type Annotation using `User` const user: User = { name: 'Simone', nickName: undefined, attributes: ['dev', 'admin'], }; // In the following lines, TypeScript won't be able to infer properly user.attributes?.map(console.log); // Property 'map' does not exist on type 'string | string[]'. Property 'map' does not exist on type 'string'. user.nickName; // string | string[] | undefined // Type assertion using `as` const user2 = { name: 'Simon', nickName: undefined, attributes: ['dev', 'admin'], } as User; // Here too, TypeScript won't be able to infer properly user2.attributes?.map(console.log); // Property 'map' does not exist on type 'string | string[]'. Property 'map' does not exist on type 'string'. user2.nickName; // string | string[] | undefined // Using `satisfies` operators we can properly infer the types now const user3 = { name: 'Simon', nickName: undefined, attributes: ['dev', 'admin'], } satisfies User; user3.attributes?.map(console.log); // TypeScript infers correctly: string[] user3.nickName; // TypeScript infers correctly: undefined Type-Only Imports and Export Type-Only Imports and Export allows you to import or export types without importing or exporting the values or functions associated with those types. This can be useful for reducing the size of your bundle. To use type-only imports, you can use the import type keyword. TypeScript permits using both declaration and implementation file extensions (.ts,.mts,.cts, and.tsx) in type-only imports, regardless of allowImportingTsExtensions settings. For example: import type { House } from './house.ts'; The following are supported forms: import type T from './mod'; import type { A, B } from './mod'; import type * as Types from './mod'; export type { T }; export type { T } from './mod'; using declaration and Explicit Resource Management A using declaration is a block-scoped, immutable binding, similar to const, used for managing disposable resources. When initialized with a value, the Symbol.dispose method of that value is recorded and subsequently executed upon exiting the enclosing block scope. This is based on ECMAScript’s Resource Management feature, which is useful for performing essential cleanup tasks after object creation, such as closing connections, deleting files, and releasing memory. Notes: Due to its recent introduction in TypeScript version 5.2, most runtimes lack native support. You’ll need polyfills for: Symbol.dispose, Symbol.asyncDispose, DisposableStack, AsyncDisposableStack, SuppressedError. Additionally, you will need to configure your tsconfig.json as follows: { "compilerOptions": { "target": "es2022", "lib": ["es2022", "esnext.disposable", "dom"] } } Example: //@ts-ignore Symbol.dispose ??= Symbol('Symbol.dispose'); // Simple polify const doWork = (): Disposable => { return { [Symbol.dispose]: () => { console.log('disposed'); }, }; }; console.log(1); { using work = doWork(); // Resource is declared console.log(2); } // Resource is disposed (e.g., `work[Symbol.dispose]()` is evaluated) console.log(3); The code will log: 1 2 disposed 3 A resource eligible for disposal must adhere to the Disposable interface: // lib.esnext.disposable.d.ts interface Disposable { [Symbol.dispose](): void; } The using declarations record resource disposal operations in a stack, ensuring they are disposed in reverse order of declaration: { using j = getA(), y = getB(); using k = getC(); } // disposes `C`, then `B`, then `A`. Resources are guaranteed to be disposed, even if subsequent code or exceptions occur. This may lead to disposal potentially throwing an exception, possibly suppressing another. To retain information on suppressed errors, a new native exception, SuppressedError, is introduced. await using declaration An await using declaration handles an asynchronously disposable resource. The value must have a Symbol.asyncDispose method, which will be awaited at the block’s end. async function doWorkAsync() { await using work = doWorkAsync(); // Resource is declared } // Resource is disposed (e.g., `await work[Symbol.asyncDispose]()` is evaluated) For an asynchronously disposable resource, it must adhere to either the Disposable or AsyncDisposable interface: // lib.esnext.disposable.d.ts interface AsyncDisposable { [Symbol.asyncDispose](): Promise; } //@ts-ignore Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose'); // Simple polify class DatabaseConnection implements AsyncDisposable { // A method that is called when the object is disposed asynchronously [Symbol.asyncDispose]() { // Close the connection and return a promise return this.close(); } async close() { console.log('Closing the connection...'); await new Promise(resolve => setTimeout(resolve, 1000)); console.log('Connection closed.'); } } async function doWork() { // Create a new connection and dispose it asynchronously when it goes out of scope await using connection = new DatabaseConnection(); // Resource is declared console.log('Doing some work...'); } // Resource is disposed (e.g., `await connection[Symbol.asyncDispose]()` is evaluated) doWork(); The code logs: Doing some work... Closing the connection... Connection closed. The using and await using declarations are allowed in Statements: for, for-in, for-of, for-await-of, switch.

Use Quizgecko on...
Browser
Browser