gibbok
typescript-book
JavaScript

The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.

Last updated Jul 9, 2026
10.3k
Stars
607
Forks
3
Issues
0
Stars/day
Attention Score
96
Language breakdown
JavaScript 35.9%
Python 28.7%
CSS 12.1%
TypeScript 7.6%
MDX 6.0%
Astro 3.3%
β–Έ Files click to expand
README

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 of 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.

I believe that high-quality technical education should be accessible to everyone, which is why I keep this book free and open.

If the book helped you squash a bug, understand a tricky concept, or advance in your career, please consider supporting my work by paying what you want (suggested price: 15 USD) or sponsoring a coffee. Your support helps me keep the content up to date and expand it with new examples and deeper explanations.

Buy Me a Coffee

Donate

Translations

This book has been translated into several languages, including:

Chinese

Italian

Portuguese (Brazil)

Swedish

Bulgarian

Downloads and website

You can also download the EPUB version:

https://github.com/gibbok/typescript-book/tree/main/downloads

An online version is available at:

https://gibbok.github.io/typescript-book

Table of Contents

- Translations - Downloads and website - 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 - 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 - Switch-true narrowing - 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 and 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\ - NoInfer\ - 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 meta-property - Dynamic Import Expressions - "tsc –watch" - Non-null Assertion Operator - Defaulted declarations - Optional Chaining - Nullish coalescing operator - Template Literal Types - Function overloading - Recursive Types - Recursive Conditional Types - ECMAScript Module Support in Node - 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 - Import Attributes - Regular Expression Syntax Checking - import defer

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 6.0.

About the Author

Simone Poggiali is an experienced Staff Engineer 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, Leroy Merlin and Snowplow have benefited from his expertise and dedication.

You can reach Simone Poggiali on the following platforms:

Full list of contributors: https://github.com/gibbok/typescript-book/graphs/contributors

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 runtime (e.g., a browser or Node.js on a server).

It supports multiple programming paradigms such as functional, generic, imperative, and object-oriented programming, and is a compiled (transpiled) language that is converted into JavaScript before execution.

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 runtime 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 potential runtime errors at compile time by allowing developers to express intent through type annotations. In addition, TypeScript can also catch certain issues even when no explicit type annotations are provided, thanks to type inference. For example, 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 to 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 runtime, 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/*.ts // Compile any .ts files under the 'src' folder to JavaScript
tsc app.ts util.ts --outfile index.js // Compile two TypeScript files (app.ts and util.ts) into a single JavaScript file (index.js)

TypeScript Configuration File

A tsconfig.json file is used to configure the TypeScript Compiler (tsc). Usually, it is added to the root of the project, together with the package.json file.

Notes:

  • tsconfig.json accepts comments even if it is in json format.
  • It is advisable to use this configuration file instead of the command-line options.
At the following link you can find the complete documentation and its schema:

https://www.typescriptlang.org/tsconfig

https://www.typescriptlang.org/tsconfig/

The following represents a list of the common and useful configurations:

target

The "target" property is used to specify which version of JavaScript ECMAScript version your TypeScript should emit/compile into. For modern browsers ES6 is a good option, for older browsers, ES5 is recommended. Note: ES5 support was removed in TypeScript 6.0.

lib

The "lib" property is used to specify which library files to include at compilation time. TypeScript automatically includes APIs for features specified in the "target" property, but it is possible to omit or pick specific libraries for particular needs. For instance, if you are working on a server project, you could exclude the "DOM" library, which is useful only in a browser environment.

strict

The "strict" option improves type safety by enabling stronger checks. It is enabled by default starting with TypeScript 6.0; otherwise, you should explicitly set it to true in your tsconfig.json. Enabling "strict" allows TypeScript to:

  • Emit code using "use strict" for each source file.
  • Consider "null" and "undefined" in the type checking process.
  • Disable the usage of the "any" type when no type annotations are present.
  • Raise an error on the usage of the "this" expression, which would otherwise imply the "any" type.

module

The "module" property sets the module system supported for the compiled program. During runtime, a module loader is used to locate and execute dependencies based on the specified module system.

The most common module loaders used in JavaScript are Node.js CommonJS for server-side applications and RequireJS for AMD modules in browser-based web applications. TypeScript can emit code for various module systems, including UMD, System, ESNext, ES2015/ES6, and ES2020. The module system should be chosen based on the target environment and the module loading mechanism available in that environment.

Note: Support for older module systems (AMD, UMD, SystemJS) was removed in TypeScript 6.0.

moduleResolution

The "moduleResolution" property specifies the module resolution strategy. Use "node" for modern TypeScript code, the "classic" strategy is used only for old versions of TypeScript (before 1.6).

esModuleInterop

The "esModuleInterop" property allows default imports from CommonJS modules that did not export using the "default" property; this property provides a shim to ensure compatibility in the emitted JavaScript. After enabling this option, we can use import MyLibrary from "my-library" instead of import * as MyLibrary from "my-library".

"esModuleInterop" was originally opt-in to avoid breaking changes, but has long been the recommended default. Disabling it can cause subtle runtime issues when using CommonJS with ESM. Note: Starting with TypeScript 6.0, this safer interop behavior is always enabled.

jsx

The "jsx" property applies only to .tsx files used in ReactJS and controls how JSX constructs are compiled into JavaScript. A common option is "preserve" which will compile to a .jsx file keeping unchanged the JSX so it can be passed to different tools like Babel for further transformations.

skipLibCheck

The "skipLibCheck" property will prevent TypeScript from type-checking the entire imported third-party packages. This property will reduce the compile time of a project. TypeScript will still check your code against the type definitions provided by these packages.

files

The "files" property indicates to the compiler a list of files that must always be included in the program.

include

The "include" property indicates to the compiler a list of files that we would like to include. This property allows glob-like patterns, such as "\*" for any subdirectory, "" for any file name, and "?" for optional characters.

exclude

The "exclude" property indicates to the compiler a list of files that should not be included in the compilation. This can include files such as "node_modules" or test files. Note: tsconfig.json allows comments.

importHelpers

TypeScript uses helper code when generating code for certain advanced or down-leveled JavaScript features. By default, these helpers are duplicated in files using them. The importHelpers option imports these helpers from the tslib module instead, making the JavaScript output more efficient.

Migration to TypeScript Advice

For large projects, it is recommended to adopt a gradual transition where TypeScript and JavaScript code will initially coexist. Only small projects can be migrated to TypeScript in one go.

The first step of this transition is to introduce TypeScript into the build chain process. This can be done by using the "allowJs" compiler option, which permits .ts and .tsx files to coexist with existing JavaScript files. As TypeScript will fall back to a type of "any" for a variable when it cannot infer the type from JavaScript files, it is recommended to disable "noImplicitAny" in your compiler options at the beginning of the migration.

The second step is to ensure that your JavaScript tests work alongside TypeScript files so that you can run tests as you convert each module. If you are using Jest, consider using ts-jest, which allows you to test TypeScript projects with Jest.

The third step is to include type declarations for third-party libraries in your project. These declarations can be found either bundled or on DefinitelyTyped. You can search for them using https://www.typescriptlang.org/dt/search and install them using:

npm install --save-dev @types/package-name

or

yarn add --dev @types/package-name

The fourth step is to migrate module by module with a bottom-up approach, following your Dependency Graph starting with the leaves. The idea is to start converting Modules that do not depend on other Modules. To visualize the dependency graphs, you can use the "madge" tool.

Good candidate modules for these initial conversions are utility functions and code related to external APIs or specifications. It is possible to automatically generate TypeScript type definitions from Swagger contracts, GraphQL or JSON schemas to be included in your project.

When there are no specifications or official schemas available, you can generate types from raw data, such as JSON returned by a server. However, it is recommended to generate types from specifications instead of data to avoid missing edge cases.

During the migration, refrain from code refactoring and focus only on adding types to your modules.

The fifth step is to enable "noImplicitAny," which will enforce that all types are known and defined, providing a better TypeScript experience for your project.

During the migration, you can use the @ts-check directive, which enables TypeScript type checking in a JavaScript file. This directive provides a loose version of type checking and can be initially used to identify issues in JavaScript files. When @ts-check is included in a file, TypeScript will try to deduce definitions using JSDoc-style comments. However, consider using JSDoc annotations only at a very early stage of the migration.

Consider keeping the default value of noEmitOnError in your tsconfig.json as false. This will allow you to output JavaScript source code even if errors are reported.

Exploring the Type System

The TypeScript Language Service

The TypeScript Language Service, also known as tsserver, offers various features such as error reporting, diagnostics, compile-on-save, renaming, go to definition, completion lists, signature help, and more. It is primarily used by integrated development environments (IDEs) to provide IntelliSense support. It seamlessly integrates with Visual Studio Code and is utilized by tools like Conquer of Completion (Coc).

Developers can leverage a dedicated API and create their own custom language service plugins to enhance the TypeScript editing experience. This can be particularly useful for implementing special linting features or enabling auto-completion for a custom templating language.

An example of a real-world custom plugin is "typescript-styled-plugin", which provides syntax error reporting and IntelliSense support for CSS properties in styled components.

For more information and quick start guides, you can refer to the official TypeScript Wiki on GitHub: https://github.com/microsoft/TypeScript/wiki/

Structural Typing

TypeScript is based on a structural type system. This means that the compatibility and equivalence of types are determined by the type's actual structure or definition, rather than its name or place of declaration, as in nominative type systems like C# or C.

TypeScript's structural type system was designed based on how JavaScript's dynamic duck typing system works during runtime.

The following example is valid TypeScript code. As you can observe, "X" and "Y" have the same member "a," even though they have different declaration names. The types are determined by their structures, and in this case, since the structures are the same, they are compatible and valid.

type X = {
    a: string;
};
type Y = {
    a: string;
};
const x: X = { a: 'a' };
const y: Y = x; // Valid

TypeScript Fundamental Comparison Rules

The TypeScript comparison process is recursive and executed on types nested at any level.

A type "X" is compatible with "Y" if "Y" has at least the same members as "X".

type X = {
    a: string;
};
const y = { a: 'A', b: 'B' }; // Valid, as it has at least the same members as X
const r: X = y;

Function parameters are compared by types, not by their names:

type X = (a: number) => void;
type Y = (a: number) => void;
let x: X = (j: number) => undefined;
let y: Y = (k: number) => undefined;
y = x; // Valid
x = y; // Valid

Function return types must be the same:

type X = (a: number) => undefined; type Y = (a: number) => number; let x: X = (a: number) => undefined; let y: Y = (a: number) => 1; y = x; // Invalid x = y; // Invalid

The return type of a source function must be a subtype of the return type of a target function:

let x = () => ({ a: 'A' }); let y = () => ({ a: 'A', b: 'B' }); x = y; // Valid y = x; // Invalid member b is missing

Discarding function parameters is allowed, as it is a common practice in JavaScript, for instance using "Array.prototype.map()":

[1, 2, 3].map((element, index, array) => element + 'x');

Therefore, the following type declarations are completely valid:

type X = (a: number) => undefined;
type Y = (a: number, b: number) => undefined;
let x: X = (a: number) => undefined;
let y: Y = (a: number) => undefined; // Missing b parameter
y = x; // Valid

Any additional optional parameters of the source type are valid:

type X = (a: number, b?: number, c?: number) => undefined;
type Y = (a: number) => undefined;
let x: X = a => undefined;
let y: Y = a => undefined;
y = x; // Valid
x = y; //Valid

Any optional parameters of the target type without corresponding parameters in the source type are valid and not an error:

type X = (a: number) => undefined;
type Y = (a: number, b?: number) => undefined;
let x: X = a => undefined;
let y: Y = a => undefined;
y = x; // Valid
x = y; // Valid

The rest parameter is treated as an infinite series of optional parameters:

type X = (a: number, ...rest: number[]) => undefined;
let x: X = a => undefined; //valid

Functions with overloads are valid if the overload signature is compatible with its implementation signature:

function x(a: string): void; function x(a: string, b: number): void; function x(a: string, b?: number): void {     console.log(a, b); } x('a'); // Valid x('a', 1); // Valid

function y(a: string): void; // Invalid, not compatible with implementation signature function y(a: string, b: number): void; function y(a: string, b: number): void { console.log(a, b); } y('a'); y('a', 1);

Function parameter comparison succeeds if the source and target parameters are assignable to supertypes or subtypes (bivariance).

// Supertype
class X {
    a: string;
    constructor(value: string) {
        this.a = value;
    }
}
// Subtype
class Y extends X {}
// Subtype
class Z extends X {}

type GetA = (x: X) => string; const getA: GetA = x => x.a;

// Bivariance does accept supertypes console.log(getA(new X('x'))); // Valid console.log(getA(new Y('Y'))); // Valid console.log(getA(new Z('z'))); // Valid

Enums are comparable and valid with numbers and vice versa, but comparing Enum values from different Enum types is invalid.

enum X {     A,     B, } enum Y {     A,     B,     C, } const xa: number = X.A; // Valid const ya: Y = 0; // Valid X.A === Y.A; // Invalid

Instances of a class are subject to a compatibility check for their private and protected members:

class X {     public a: string;     constructor(value: string) {         this.a = value;     } }

class Y { private a: string; constructor(value: string) { this.a = value; } }

let x: X = new Y('y'); // Invalid

The comparison check does not take into consideration the different inheritance hierarchy, for instance:

class X {
    public a: string;
    constructor(value: string) {
        this.a = value;
    }
}
class Y extends X {
    public a: string;
    constructor(value: string) {
        super(value);
        this.a = value;
    }
}
class Z {
    public a: string;
    constructor(value: string) {
        this.a = value;
    }
}
let x: X = new X('x');
let y: Y = new Y('y');
let z: Z = new Z('z');
x === y; // Valid
x === z; // Valid even if z is from a different inheritance hierarchy

Generics are compared using their structures based on the resulting type after applying the generic parameter, only the final result is compared as a non-generic type.

interface X<T> {     a: T; } let x: X<number> = { a: 1 }; let y: X<string> = { a: 'a' }; x === y; // Invalid as the type argument is used in the final structure

interface X<T> {}
const x: X<number> = 1;
const y: X<string> = 'a';
x === y; // Valid as the type argument is not used in the final structure

When generics do not have their type argument specified, all the unspecified arguments are treated as types with "any":

type X = <T>(x: T) => T;
type Y = <K>(y: K) => K;
let x: X = x => x;
let y: Y = y => y;
x = y; // Valid

Remember:

let a: number = 1; let b: number = 2; a = b; // Valid, everything is assignable to itself

let c: any; c = 1; // Valid, all types are assignable to any

let d: unknown; d = 1; // Valid, all types are assignable to unknown

let e: unknown; let e1: unknown = e; // Valid, unknown is only assignable to itself and any let e2: any = e; // Valid let e3: number = e; // Invalid

let f: never; f = 1; // Invalid, nothing is assignable to never

let g: void; let g1: any; g = 1; // Invalid, void is not assignable to or from anything except any g = g1; // Valid

Please note that when "strictNullChecks" is enabled, "null" and "undefined" are treated similarly to "void"; otherwise, they are similar to "never".

Types as Sets

In TypeScript, a type is a set of possible values. This set is also referred to as the domain of the type. Each value of a type can be viewed as an element in a set. A type establishes the constraints that every element in the set must satisfy to be considered a member of that set. The primary task of TypeScript is to check and verify whether one set is a subset of another.

TypeScript supports various types of sets:

| Set term | TypeScript | Notes | | ------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Empty set | never | "never" contains anything apart from itself | | Single element set | undefined / null / literal type | | | Finite set | boolean / union | | | Infinite set | string / number / object | | | Universal set | any / unknown | Every element is a member of "any" and every set is a subset of it / "unknown" is a type-safe counterpart of "any" |

Here are a few examples:

| TypeScript | Set term | Example | | --------------------- | ---------------------- | ------------------------------------------------------------------------------- | | never | βˆ… (empty set) | const x: never = 'x'; // Error: Type 'string' is not assignable to type 'never' | | | | | Literal type | Single element set | type X = 'X'; | | | | type Y = 7; | | | | | Value assignable to T | Value ∈ T (member of) | type XY = 'X' \| 'Y'; | | | | const x: XY = 'X'; | | | | | T1 assignable to T2 | T1 βŠ† T2 (subset of) | type XY = 'X' \| 'Y'; | | | | const x: XY = 'X'; | | | | const j: XY = 'J'; // Type '"J"' is not assignable to type 'XY'. | | | | | | T1 extends T2 | T1 βŠ† T2 (subset of) | type X = 'X' extends string ? true : false; | | | | | T1 \| T2 | T1 βˆͺ T2 (union) | type XY = 'X' \| 'Y'; | | | | type JK = 1 \| 2; | | | | | T1 & T2 | T1 ∩ T2 (intersection) | type X = \{ a: string \} | | | | type Y = \{ b: string \} | | | | type XY = X & Y | | | | const x: XY = \{ a: 'a', b: 'b' \} | | | | | unknown | Universal set | const x: unknown = 1 |

A union, (T1 | T2), creates a wider set (both):

type X = {
    a: string;
};
type Y = {
    b: string;
};
type XY = X | Y;
const r: XY = { a: 'a', b: 'x' }; // Valid

An intersection, (T1 & T2), creates a narrower set (only shared):

type X = {     a: string; }; type Y = {     a: string;     b: string; }; type XY = X & Y; const r: XY = { a: 'a' }; // Invalid const j: XY = { a: 'a', b: 'b' }; // Valid

The extends keyword can be considered as "subset of" in this context. It sets a constraint for a type. When extends is used with a generic, it constrains the generic type parameter to a more specific type.

Please note that extends here has nothing to do with class inheritance in an OOP sense.

TypeScript works with structural types and does not have a strict nominal hierarchy. In fact, as in the example below, two types can overlap without either being a subtype of the other, because TypeScript considers the structure, or shape, of objects.

interface X {
    a: string;
}
interface Y extends X {
    b: string;
}
interface Z extends Y {
    c: string;
}
const z: Z = { a: 'a', b: 'b', c: 'c' };
interface X1 {
    a: string;
}
interface Y1 {
    a: string;
    b: string;
}
interface Z1 {
    a: string;
    b: string;
    c: string;
}
const z1: Z1 = { a: 'a', b: 'b', c: 'c' };

const r: Z1 = z; // Valid

Assign a type: Type Declarations and Type Assertions

A type can be assigned in different ways in TypeScript:

Type Declaration

In the following example, we use x: X (": Type") to declare a type for the variable x.

type X = {
    a: string;
};

// Type declaration const x: X = { a: 'a', };

If the variable is not in the specified format, TypeScript will report an error. For instance:

type X = {     a: string; };

const x: X = { a: 'a', b: 'b', // Error: Object literal may only specify known properties };

Type Assertion

It is possible to add an assertion by using the as keyword. This tells the compiler that the developer has more information about a type and silences any errors that may occur.

For example:

type X = {
    a: string;
};
const x = {
    a: 'a',
    b: 'b',
} as X;

In the above example, the object x is asserted to have the type X using the as keyword. This informs the TypeScript compiler that the object conforms to the specified type, even though it has an additional property b not present in the type definition.

Type assertions are useful in situations where a more specific type needs to be specified, especially when working with the DOM. For instance:

const myInput = document.getElementById('my_input') as HTMLInputElement;

Here, the type assertion as HTMLInputElement is used to tell TypeScript that the result of getElementById should be treated as an HTMLInputElement. Type assertions can also be used to remap keys, as shown in the example below with template literals:

type J<Type> = {
    [Property in keyof Type as prefix_${string &amp;
        Property}]: () => Type[Property];
};
type X = {
    a: string;
    b: number;
};
type Y = J<X>;

In this example, the type J<Type> uses a mapped type with a template literal to remap the keys of Type. It creates new properties with a "prefix_" added to each key, and their corresponding values are functions returning the original property values.

It is worth noting that when using a type assertion, TypeScript will not execute excess property checking. Therefore, it is generally preferable to use a Type Declaration when the structure of the object is known in advance.

Ambient Declarations

Ambient declarations are files that describe types for JavaScript code, they have a file name format as .d.ts.. They are usually imported and used to annotate existing JavaScript libraries or to add types to existing JS files in your project.

Many common libraries types can be found at: https://github.com/DefinitelyTyped/DefinitelyTyped/

and can be installed using:

npm install --save-dev @types/library-name

For your defined Ambient Declarations, you can import using the "triple-slash" reference:

/// <reference path="./library-types.d.ts" />

You can use Ambient Declarations even within JavaScript files using // @ts-check.

The declare keyword enables type definitions for existing JavaScript code without importing it, serving as a placeholder for types from another file or globally.

Property Checking and Excess Property Checking

TypeScript is based on a structural type system but excess property checking is a property of TypeScript which allows it to check whether an object has the exact properties specified in the type.

Excess Property Checking is performed when assigning object literals to variables or when passing them as arguments to the function's excess property, for instance.

type X = {     a: string; }; const y = { a: 'a', b: 'b' }; const x: X = y; // Valid because structural typing const w: X = { a: 'a', b: 'b' }; // Invalid because excess property checking

Weak Types

A type is considered weak when it contains nothing but a set of all-optional properties:

type X = {
    a?: string;
    b?: string;
};

TypeScript considers an error to assign anything to a weak type when there is no overlap, for instance, the following throws an error:

type Options = {     a?: string;     b?: string; };

const fn = (options: Options) => undefined;

fn({ c: 'c' }); // Invalid

Although not recommended, if needed, it is possible to bypass this check by using type assertion:

type Options = {
    a?: string;
    b?: string;
};
const fn = (options: Options) => undefined;
fn({ c: 'c' } as Options); // Valid

Or by adding unknown to the index signature to the weak type:

type Options = {
    [prop: string]: unknown;
    a?: string;
    b?: string;
};

const fn = (options: Options) => undefined; fn({ c: 'c' }); // Valid

Strict Object Literal Checking (Freshness)

Strict object literal checking, sometimes referred to as "freshness", is a feature in TypeScript that helps catch excess or misspelled properties that would otherwise go unnoticed in normal structural type checks.

When creating an object literal, the TypeScript compiler considers it "fresh." If the object literal is assigned to a variable or passed as a parameter, TypeScript will throw an error if the object literal specifies properties that do not exist in the target type.

However, "freshness" disappears when an object literal is widened or a type assertion is used.

Here are some examples to illustrate:

type X = { a: string }; type Y = { a: string; b: string };

let x: X; x = { a: 'a', b: 'b' }; // Freshness check: Invalid assignment var y: Y; y = { a: 'a', bx: 'bx' }; // Freshness check: Invalid assignment

const fn = (x: X) => console.log(x.a);

fn(x); fn(y); // Widening: No errors, structurally type compatible

fn({ a: 'a', bx: 'b' }); // Freshness check: Invalid argument

let c: X = { a: 'a' }; let d: Y = { a: 'a', b: '' }; c = d; // Widening: No Freshness check

Type Inference

TypeScript can infer types when no annotation is provided during:

  • Variable initialization.
  • Member initialization.
  • Setting defaults for parameters.
  • Function return type.
For example:
let x = 'x'; // The type inferred is string

The TypeScript compiler analyzes the value or expression and determines its type based on the available information.

More Advanced Inferences

When multiple expressions are used in type inference, TypeScript looks for the "best common types." For instance:

let x = [1, 'x', 1, null]; // The type inferred is: (string | number | null)[]

If the compiler cannot find the best common types, it returns a union type. For example:

let x = [new RegExp('x'), new Date()]; // Type inferred is: (RegExp | Date)[]

TypeScript utilizes "contextual typing" based on the variable's location to infer types. In the following example, the compiler knows that e is of type MouseEvent because of the click event type defined in the lib.d.ts file, which contains ambient declarations for various common JavaScript constructs and the DOM:

window.addEventListener('click', function (e) {}); // The inferred type of e is MouseEvent

Type Widening

Type widening is the process in which TypeScript assigns a type to a variable initialized without a type annotation. It allows narrow to wider types but not vice versa. In the following example:

let x = 'x'; // TypeScript infers as string, a wide type let y: 'y' | 'x' = 'y'; // y types is a union of literal types y = x; // Invalid Type 'string' is not assignable to type '"x" | "y"'.

TypeScript assigns string to x based on the single value provided during initialization (x), this is an example of widening.

TypeScript provides ways to have control of the widening process, for instance using "const".

Const

Using the const keyword when declaring a variable results in a narrower type inference in TypeScript.

For example:

const x = 'x'; // TypeScript infers the type of x as 'x', a narrower type
let y: 'y' | 'x' = 'y';
y = x; // Valid: The type of x is inferred as 'x'

By using const to declare the variable x, its type is narrowed to the specific literal value 'x'. Since the type of x is narrowed, it can be assigned to the variable y without any error. The reason the type can be inferred is because const variables cannot be reassigned, so their type can be narrowed down to a specific literal type, in this case, the literal type 'x'.

Const Modifier on Type Parameters

From version 5.0 of TypeScript, it is possible to specify the const attribute on a generic type parameter. This allows for inferring the most precise type possible. Let's see an example without using const:

function identity<T>(value: T) {
    // No const here
    return value;
}
const values = identity({ a: 'a', b: 'b' }); // Type inferred is: { a: string; b: string; }

As you can see, the properties a and b are inferred with a type of string .

Now, let's see the difference with the const version:

function identity<const T>(value: T) {
    // Using const modifier on type parameters
    return value;
}
const values = identity({ a: 'a', b: 'b' }); // Type inferred is: { a: "a"; b: "b"; }

Now we can see that the properties a and b are inferred as string literals rather than just string types.

Const assertion

This feature allows you to declare a variable with a more precise literal type based on its initialization value, signifying to the compiler that the value should be treated as an immutable literal. Here are a few examples:

On a single property:

const v = {
    x: 3 as const,
};
v.x = 3;

On an entire object:

const v = {
    x: 1,
    y: 2,
} as const;

This can be particularly useful when defining the type for a tuple:

const x = [1, 2, 3]; // number[]
const y = [1, 2, 3] as const; // Tuple of readonly [1, 2, 3]

Explicit Type Annotation

We can be specific and pass a type. In the following example, property x is of type number:

const v = {
    x: 1, // Inferred type: number (widening)
};
v.x = 3; // Valid

We can make the type annotation more specific by using a union of literal types:

const v: { x: 1 | 2 | 3 } = {     x: 1, // x is now a union of literal types: 1 | 2 | 3 }; v.x = 3; // Valid v.x = 100; // Invalid

Type Narrowing

Type Narrowing is the process in TypeScript where a general type is narrowed down to a more specific type. This occurs when TypeScript analyzes the code and determines that certain conditions or operations can refine the type information.

Narrowing types can occur in different ways, including:

Conditions

By using conditional statements, such as if or switch, TypeScript can narrow down the type based on the outcome of the condition. For example:

let x: number | undefined = 10;

if (x !== undefined) { x += 100; // The type is number, which had been narrowed by the condition }

Throwing or returning

Throwing an error or returning early from a branch can be used to help TypeScript narrow down a type. For example:

let x: number | undefined = 10;

if (x === undefined) { throw 'error'; } x += 100;

Other ways to narrow down types in TypeScript include:

  • instanceof operator: Used to check if an object is an instance of a specific class.
  • in operator: Used to check if a property exists in an object.
  • typeof operator: Used to check the type of a value at runtime.
  • Built-in functions like Array.isArray(): Used to check if a value is an array.

Discriminated Union

Using a "Discriminated Union" is a pattern in TypeScript where an explicit "tag" is added to objects to distinguish between different types within a union. This pattern is also referred to as a "tagged union." In the following example, the "tag" is represented by the property "type":

type A = { type: 'type_a'; value: number };
type B = { type: 'type_b'; value: string };

const x = (input: A | B): string | number => { switch (input.type) { case 'type_a': return input.value + 100; // type is A case 'type_b': return input.value + 'extra'; // type is B } };

User-Defined Type Guards

In cases where TypeScript is unable to determine a type, it is possible to write a helper function known as a "user-defined type guard." In the following example, we will utilize a Type Predicate to narrow down the type after applying certain filtering:

const data = ['a', null, 'c', 'd', null, 'f'];

const r1 = data.filter(x => x != null); // The type is (string | null)[], TypeScript was not able to infer the type properly

const isValid = (item: string | null): item is string => item !== null; // Custom type guard

const r2 = data.filter(isValid); // The type is fine now string[], by using the predicate type guard we were able to narrow the type

Switch-true narrowing

TypeScript 5.3 adds switch-true narrowing, letting you replace messy if/else chains with switch (true) using boolean conditions. It improves readability and still narrows types. It’s similar to pattern matching, but simpler.

```typescript function classify(x: unknown) { switch (true) { case typeof x === 'string': retur


README truncated. View on GitHub

Β© 2026 GitRepoTrend Β· gibbok/typescript-book Β· Updated daily from GitHub