Table of Contents
- 1.1 Introduction
- 1.2 Does typing really matter?
- 1.3 Version control with TypeScript and JavaScript
- 1.4 Demo: How to version TypeScript
- 1.5 Demo: Debugging TypeScript
- 2.1 How types work in TypeScript
- 2.2 TypeScript “types”
- 2.3 Demo: TypeScript typing vs JavaScript typing
- 2.4 Demo: A unit test avoided
- 2.5 Some more tips with types
- 3.1 What are interfaces used for
- 3.2 Demo: Implement a third-party TS interface
- 3.3 What are generics used for
- 3.4 Demo: Implement a generic in TypeScript
- 4.1 Run the compiler and use its options
- 4.2 Demo: A Closer Look at Transpilation
- 4.3 Configure your project with tsconfig files
- 4.4 Demo: Working with tsconfig files
- 4.5 Use type declaration files
- 4.6 Demo: Type inference from declaration files
- 5.1 The Observer and Pub-Sub patterns
- 5.2 Our JavaScript client application
- 5.3 Demo: Migrate client JS to TypeScript
- 5.4 Demo: Migrate client JS to TypeScript, Part 2
- 5.5 Demo: Migrate server JS to TypeScript
1. Understanding the “Why” of TypeScript
1.1 Introduction
The instructor, Chris B. Behrens, has been a developer and technologist for approximately 30 years. The training first offers a little theory, then moves very quickly to practical application and code. All of the code used in the course is available in a public GitHub repository, free to use for any benevolent use, including commercial.
The highlight of the training is a massive port/refactoring of a JavaScript application to TypeScript, making the code logic much more explicit in the process. It is strongly recommended to clone the repository locally to be able to view the code in parallel.
The historical context of JavaScript
JavaScript was first introduced as LiveScript in a beta version of Netscape Navigator in 1995. It is, by many measures, the most popular programming language of all time. Virtually every developer — backend, frontend, DevOps, DBA — knows at least a little JavaScript.
JavaScript, however, has evolved in an ad hoc manner, with changes made reactively as uses emerge. This approach promotes widespread adoption, but comes at the expense of a cohesive design philosophy that would unite the entire system.
What TypeScript is
TypeScript is an effort to create a superset of JavaScript that unifies the language and, importantly, introduces a more formal type system. The name says it all: TypeScript.
For developers from traditional languages (C#, Java), TypeScript reintroduces familiar tools: legacy, generics, strong typing. TypeScript is a layer on top of JavaScript designed to reintroduce these tools in a familiar way, allowing for example a C# developer to work with familiar constructs.
TypeScript is not “C# over JavaScript”
It is true that C# and TypeScript were both designed by the same man, Anders Hejlsburg. However, TypeScript does not seek to undo the conventions of JavaScript programming. TypeScript remains “JavaScript-ic” — it does not force constructs that do not lend themselves to it. Python uses the term “Pythonic” to describe that which is consistent with the Python ethos; TypeScript aims for a similar consistency with the JavaScript spirit while adding type rigor.
1.2 Does typing really matter?
There is significant debate in computer science about whether strong typing (or static typing) is necessary. The terms “static typing” and “strong typing” are not exactly equivalent. The best prism to resolve this debate is the notion of type coercion.
Definition of type coercion
Type coercion occurs when you have a Type A variable and need to make it work somehow with Type B, which may or may not be related to Type A.
Simple example in JavaScript:
let x = 5; // x est un nombre
x = "drum solo"; // x devient une chaîne de caractères
console.log(x); // affiche "drum solo" sans aucune erreur
The soft typing vs strict typing debate
This debate essentially boils down to a trade-off between two good things — or two bad things, depending on perspective.
-
Argument for flexible typing: It is sometimes useful to force to a new type. A strict type declaration is a commitment, and in general a developer should commit as little as possible to solving a problem, so that commitment doesn’t interfere with the solution later.
-
Argument against coercion: In practice, type coercion is often a code smell, a sign that an idea has not been fully thought through. Indirect proof: JavaScript linters recommend
constrather thanvar— the idea being that under most circumstances the value of a variable doesn’t really vary unless there’s an error.
TypeScript’s position is clear: type coercion is generally a sign of a design problem, not a desirable feature.
1.3 Version control with TypeScript and JavaScript
The trainer had to work on systems where it was necessary to version binaries and intermediate build artifacts, because the build had to be done on a machine without Internet access after cloning the repository. This situation is uncomfortable for any good developer.
The fundamental principle of version control
We only put the purely non-deterministic elements of the system under version control.
Deterministic = a result that follows purely from something else. Example: 346 × 183 = 63,318 always. We do not need to version 63,318, because it is the deterministic result of 346 and 183. Similarly, the JavaScript emitted is the deterministic result of compiling the source TypeScript.
Application to TypeScript: The source TypeScript is non-deterministic (this is what the developer writes). The JavaScript emitted is deterministic (result of compilation). We must therefore only version the .ts files, not the issued .js files. We add *.js in the .gitignore.
The important exception
If you have only partially migrated your code to TypeScript (mix of .ts and .js), adding *.js globally in the .gitignore will cause problems — you will also remove .js files that are not generated by TypeScript.
In this case, two options:
- Explicitly add in the
.gitignorethe specific.jsfiles which are generated by TypeScript - Be careful at the Git command line
The trainer prefers more robust approaches than “be careful”, but sometimes this is the price to pay during the transition.
1.4 Demo: How to version TypeScript
Starting situation
A simple JavaScript application reads an array of Employee objects and displays their data in the browser, with a simple HTML page.
File system structure: a single .js file and a single .html file.
Installing TypeScript
npm install -g typescript
The -g flag installs TypeScript globally on the machine, available for all projects.
Verifying the installation:
tsc
If TypeScript is installed, the command responds with usage options.
First migration: rename .js to .ts
The first migration step is simply to rename the .js file to .ts. That’s all. The file is now TypeScript.
Note: the .html file references a JavaScript file, so it will be necessary to ensure that the compilation generates the expected .js.
First compilation and errors
tsc nomDuFichier.ts
Result: 8 errors, all related to properties of the Employee class, because TypeScript does not automatically make properties available like JavaScript does with Expando properties.
In JavaScript, you can add properties to an object dynamically:
function Employee() {}
employee.firstName = "John"; // Expando property - valide en JS, invalide en TS
In TypeScript, properties must be explicitly declared:
class Employee {
firstName: string;
lastName: string;
employeeId: string;
// ...
}
This is the first concrete introduction to types in TypeScript: the three properties are declared as string.
After correction
Recompile:
tsc nomDuFichier.ts
No output = success. The JavaScript emitted looks almost exactly as it did before. The git diff command only shows spacing changes. The app in the browser still works.
1.5 Demo: Debugging TypeScript
The initial debugging problem
When compiling TypeScript into JavaScript, the browser only downloads the .js file. In the Source tab of the browser’s debugging tools, we see the HTML file and the JavaScript file — but no TypeScript file, because it was never downloaded.
We can always debug the JavaScript emitted directly (put a breakpoint, refresh), but it is suboptimal: you have to mentally do the mapping to the TypeScript.
Source Maps
More elegant solution: source maps.
Recompile with the sourceMap option:
tsc --sourceMap true nomDuFichier.ts
A .map file is now generated in the directory. When you refresh the browser, the tools detect the map file and automatically translate the breakpoints from JavaScript to TypeScript. We can now debug directly in TypeScript. If we find a problem, we fix it in the exact TypeScript code we see — without having to do mind mapping.
Creating the tsconfig.json file
tsc --init
This command (with the two hyphens) is different from git init. It creates the tsconfig.json configuration file. The file name is always tsconfig.json — this is the only name the compiler automatically recognizes.
At this stage, the trainer also presents Visual Studio Community Edition (the free version) to take advantage of syntax highlighting, while continuing to compile on the command line.
Compiling with tsconfig
tsc -p .
The -p flag (for project) designates the current folder. With the tsconfig.json in place, TypeScript enforces stricter rules. Errors appear because the types are not yet all specified in the constructor.
Fix: add type decorators in the constructor:
constructor(firstName: string, lastName: string, employeeId: string) {
// ...
}
Please note: if employeeId is declared as string but the values passed in the table are numbers without quotes (eg: 42 instead of "42"), TypeScript will refuse compilation.
Source Map via tsconfig
In tsconfig.json, uncomment the sourceMap option:
{
"compilerOptions": {
"sourceMap": true
}
}
Recompile and the .map file is automatically generated.
InlineSourceMap (alternative option)
The inlineSourceMap option encodes the source map directly in the JavaScript file in base64:
{
"compilerOptions": {
"inlineSourceMap": true
}
}
Important note:
sourceMapandinlineSourceMapare mutually exclusive — you cannot enable both at the same time.
With inlineSourceMap, there is no separate .map file. At the end of the emitted .js file, there is a comment # sourceMappingURL=data:application/json;base64,... with all the encoded information.
Debugging works exactly the same in the browser, with breakpoints and watches. Chromium tools have natively supported all of this for many years.
2. Exploring type annotations and type inference
2.1 How types work in TypeScript
When is the type determined?
In statically or strongly typed languages, the type of a variable is determined at compilation (compile time).
C# Example — this code causes a compilation error:
int x = 5;
x = "Chris B. Behrens"; // Erreur : ne peut pas assigner une string à un int
Example of type conversion in C# (not coercion — real conversion work):
int age;
string rawAge = request["age"]; // provient d'un formulaire web (string)
age = int.Parse(rawAge); // conversion explicite string → int
C# casting example:
int x = 10;
double squared = Math.Pow(x, 2); // Math.Pow retourne double (conservatif)
int squaredInt = (int)squared; // Cast valide: 100.0 == 100, pas de perte
Math.Pow returns double (the largest numeric type in C#) out of conservatism to avoid overflow errors when multiplying large numbers, and to have decimal places if necessary.
JavaScript has types — but dynamic
JavaScript does have types, but they are automatically checked and converted when necessary:
let x = 5;
x = "Chris B. Behrens"; // Parfaitement valide en JavaScript
console.log(typeof x); // "string"
The same thing in C# is possible with the dynamic keyword — and the keyword itself is telling: the type can change at any time, and that’s acceptable.
let age = 16;
console.log(typeof age); // "number"
age = "seize";
console.log(typeof age); // "string" — le type a changé !
It’s not that there aren’t types in JavaScript, it’s that the type is dynamic — it can change.
The position of TypeScript
The ability to change the type at will is a bad thing. TypeScript takes this position clearly. The instructor also works in Python (another dynamically typed language), and he notes that there are type annotation packages for Python — proof that even dynamic language communities recognize the value of typing.
The principle of broken windows in criminology applies here: tolerating small breaches (like dynamic typing) is an invitation to bigger problems. Static typing imposes a rigor that forces you to think more fully about types from the start. Dynamic typing seeks to avoid this work, but ultimately creates a “broken windows” situation for all but the most experienced developers.
2.2 TypeScript “types”
The three primitives
TypeScript relies on three main primitive types:
| Type | Description |
|---|---|
string | A series of text characters. Largest values. Useful for parsing files, starting point before conversion. |
number | A numerical value. For mathematical operations or any other numerical use. |
boolean | Value true or false. |
The problem of truthiness in JavaScript
JavaScript has a concept called truthiness:
In JavaScript, a truthy value is a value considered
truein a Boolean context. All values are truthy except valsy:false,0,-0,0n,"",null,undefined,NaN, anddocument.all.
This concept of dynamic typing applied to Booleans is contrary to what TypeScript seeks to accomplish. TypeScript instead uses explicit booleans as its data type.
The any type — to avoid
There is a fourth type: any. By using it, we are three quarters back to dynamic typing.
The author considers this to be bad code and would reject it in a pull request review. If you are tempted to use any, take a moment to reconsider your code. If you really want to work with an any type, you might as well write native JavaScript.
// Exemple à éviter
let looselyTyped: any = 4;
looselyTyped.ifItExists(); // TypeScript ne vérifie pas ça
looselyTyped.toFixed(); // TypeScript ne vérifie pas ça non plus
Additional types
- Framework types like
Dateare valid TypeScript types - Custom types created by developer
- Pseudo-types:
NaN,undefined,null,void(for function returns)
Type annotation syntax
The type of a variable is declared with a type annotation after the colon:
let firstName: string;
let age: number;
let isActive: boolean;
function greet(name: string): string {
return `Hello, ${name}`;
}
The return type of a function is optional by default — TypeScript infers the return type from the return, or returns any if inference is impossible.
2.3 Demo: TypeScript typing vs JavaScript typing
The example application
This is a simulation of a real project: temperature sensors connected in IoT (Internet of Things) in a manufacturing and mining company in Minnesota. The app includes:
temp.js(named for temperature, not temporary): A simple Node.js server that opens port 2112 for WebSockets, sends a simulated data packet, and logs client connections/disconnects.client.html: A simple client HTML page that connects to the WebSocket, parses the data packet, and displays the value on a real-time graph.
The dataPacket class (already migrated to TypeScript)
class dataPacket {
private _sensorId: string;
private _time: number;
private _value: number;
static readonly DECIMAL_PLACES: number = 2;
static readonly MIN_TEMP: number = -40;
static readonly MAX_TEMP: number = 112;
set sensorId(value: string) { // L'argument doit être une string
this._sensorId = value;
}
set time(value: number) { // L'argument doit être un number
this._time = value;
}
set value(value: number) {
if (value < dataPacket.MAX_TEMP) { // Règle métier : nettoyage des données bruitées
this._value = value;
}
}
// Trois fonctions avec types de retour inférés (pas d'annotation explicite)
}
Notable points:
- Setters for
sensorIdandtimehave explicitly annotated types - The setter for
valueapplies a business rule: the sensor can return spurious values which have no physical value (above 112°C) — this setter cleans the data - All three methods have no return type annotation — TypeScript infers them
2.4 Demo: A unit test avoided
One of the great advantages of strong typing
Strong typing allows eliminating unit tests associated with validating that values correspond to expected types.
JavaScript example (without TypeScript)
Person object in JavaScript, intentionally similar to what TypeScript would produce:
function Person(name, age) {
this.name = name;
this.age = this._validateAge(age); // setter validateur
}
Person.prototype.isNumber = function(value) {
return typeof value === "number";
};
Person.prototype.isValidAge = function(age) {
return this.isNumber(age) && age >= 0 && age < 120;
};
These two functions are called in the age setter to validate the value.
Necessary Unit Testing in JavaScript
With the tape framework (alternative to Mocha, Jasmine, etc.):
var Person = require('./person');
var test = require('tape');
test('isNumber tests', function(t) {
t.plan(3);
t.ok(person.isNumber(16));
t.ok(person.isNumber(0));
t.notOk(person.isNumber("seize"));
});
test('isValidAge tests', function(t) {
t.plan(3);
t.ok(person.isValidAge(16));
t.ok(person.isValidAge(0));
t.notOk(person.isValidAge(200));
});
test('Person constructor with various ages', function(t) {
t.plan(2);
var p1 = new Person("Alice", 16); // valide, rien ne se passe
var p2 = new Person("Bob", "seize"); // type invalide, devrait lancer une exception
var p3 = new Person("Carol", 200); // âge invalide selon les règles métier
t.ok(p2.age === undefined);
t.ok(p3.age === undefined);
});
What TypeScript eliminates
With TypeScript, the constructor parameter age is declared as number. TypeScript refuses at compilation to pass a string where a number is expected. All the logic of isNumber and the unit tests that check it become useless — the compiler does this work automatically.
Only tests linked to business rules (age between 0 and 120) remain necessary, because these are not type constraints but logical constraints.
2.5 Some more tricks with types
Type inference (type inference)
TypeScript can infer the type of a variable from the value on the right side of the assignment:
let age = 16; // TypeScript infère : age est de type number
console.log(typeof age); // "number"
let name = "Chris"; // TypeScript infère : name est de type string
console.log(typeof name); // "string"
Important: Once age is declared with the value 16, TypeScript expects age to be numeric for its entire lifetime. These two statements cannot follow each other:
let age = 16;
age = "seize"; // Erreur TypeScript — age est inféré comme number
Union Types
TypeScript allows types to be composed together to form complex types:
let status: "pending" | "approved" | "rejected";
// status ne peut prendre que l'une de ces trois valeurs
However, the author thinks that this kind of problem is better served by an enum:
enum Status {
Pending,
Approved,
Rejected
}
let orderStatus: Status = Status.Pending;
Both syntaxes are valid, but enums are more expressive and maintainable.
3. Working with Generics and Interfaces
3.1 What are interfaces used for?
The usual explanation — and what it leaves out
The classic explanation of an interface is that it is a contract to implement certain members. It’s not wrong, but it doesn’t really speak to the underlying problem that this contract solves.
The fundamental truth about interfaces:
The interface ensures that certain methods or properties are in place so that something else can call them and do its job.
We only really understand a tool when we have needed to use it.
Concrete example: logging
One of the simplest and most used examples of interfaces is logging.
Let’s assume that Globomantic products work with an interface called ILogger:
interface ILogger {
LogEvent(event: string, moment: number): void;
LogException(exception: Error, locals: any[], moment: number): void;
}
These two methods are called by a Universal LogWatcher which monitors a system and receives event and exception callbacks.
The benefit of the interface
By keeping the specifics of how items are logged decoupled from the LogWatcher, one can defer these decisions from the design phase of the LogWatcher.
Concretely: if I design this logging framework, I could deliver with a text logger, an XML logger, a JSON logger, a database logger for the most popular database formats. But in reality, there will be log targets that I can’t predict — if only because they don’t yet exist at the time of delivery.
It may be useful to ship a few common interfaces to save developers work, but the main thing is to provide them with the tools to create their own implementations — and that’s exactly what the interface does.
3.2 Demo: Implement a third-party TS interface
The ILogger interface
export interface ILogger {
title: string;
LogEvent(event: string, moment: number): void;
LogError(error: Error, locals: any[], moment: number): void;
}
Key points:
exportmakes the interface usable by other files (like in a shipped application)- A
titleproperty to identify the logger LogEvent: logs an event with amomenttimestamp (value in ticks)LogError: logs an error with an additionallocalstable (local variables at the time of the exception — very useful for debugging)
The EventHubLogger implementation
import { EventHubClient } from 'azure-event-hubs';
import { ILogger } from './ilogger';
export class EventHubLogger implements ILogger {
title: string; // Propriété requise par l'interface
constructor() {
this.title = "EventHubLogger"; // Identifie ce logger pour le LogWatcher
}
LogEvent(event: string, moment: number): void {
const manager = new EventHubClient(...);
manager.connect();
const eventObj = { event, moment };
manager.send(JSON.stringify(eventObj)); // stringify pour EventHub
manager.close();
}
LogError(error: Error, locals: any[], moment: number): void {
const manager = new EventHubClient(...);
manager.connect();
const eventObj = { error, locals, moment }; // appende la collection locals
manager.send(JSON.stringify(eventObj));
manager.close();
}
}
Key points:
- The
implementskeyword tells TypeScript that an interface follows - TypeScript checks at compile time that all members of the interface are implemented
- If a method is missing or has an incorrect signature, the compiler reports an error
Compilation demonstration
Before compilation: No JavaScript files in the directory. After tsc: .js files are emitted. If the implementation does not fully satisfy the interface, compilation fails with explicit error messages.
3.3 What are generics used for?
The problem with any
The objective of generics in TypeScript is simple: avoid the any type.
To illustrate with the example of logging: suppose a writer who writes to any target:
function writeToLog(value: string): void {
// ...
}
That’s good. But if we want to write numbers:
writeToLog(42); // Erreur : l'argument doit être de type string
We could create overloads:
function writeToLog(value: string): void;
function writeToLog(value: number): void;
function writeToLog(value: boolean): void;
// Cela devient vite ingérable...
Or use any:
function writeToLog(value: any): void {
// Ça marche, mais on perd toute l'information de type
}
The problem with any: when we use this universal signature, we lose information on the type of data with which we are working. We can no longer take advantage of compile-time type checks.
The formatter makes a firm rule: avoid any universally in his TypeScript. If one is tempted to use any, it is best to take a moment to reconsider the code architecture.
The solution with generics
function writeToLog<T>(value: T): void {
// T est un placeholder pour le type que le consommateur va fournir
}
Generics allow you to obtain the benefits of typing while deferring the decision on the type to the consumer developer.
3.4 Demo: Implement a generic in TypeScript
Interface kvp (Key-Value Pair)
A key-value pair is useful for creating a dictionary-type object where you can reference an item by its key without having to iterate over the entire set.
interface kvp<V> {
key: string; // La clé est toujours une string
value: V; // Le type de la valeur est différé au consommateur
}
The <V> at the top declares the generic type parameter. V is a placeholder — a stand-in for the actual type that will be provided during invocation.
Function createkvp
function createkvp<V>(key: string, value: V): kvp<V> {
return { key, value };
}
To understand: momentarily ignoring the <V>, it is a simple function which takes a key (string) and a value (of type V), and returns a kvp object. The only mystery is the V type, which comes from the invocation.
Concrete Invocations
// Paire clé-valeur numérique
const numberPair: kvp<number> = createkvp("meaning of life", 42);
// Paire clé-valeur booléenne
const boolPair: kvp<boolean> = createkvp("knows typescript", true);
Generic type inference
TypeScript can infer generic type from passed arguments. In the numberPair example, we don’t need to explicitly write createkvp<number> — TypeScript sees that 42 is a number and infers V = number automatically.
However, we can be explicit if we wish:
const numberPair = createkvp<number>("meaning of life", 42);
Both forms are valid. The first is more concise; the second is more explicit for readability.
4. Choose configuration and compilation options
4.1 Running the compiler and using its options
The TypeScript compilation process
Code TypeScript → Compilateur → JavaScript
This translation (transpilation/compilation) is the ultimate description of the process: carry across from one to another — carry from one language to another.
Question: can we run TypeScript without compiling?
At the time of writing this course: no, it’s impossible. There is, however, a package called ts-node that appears to run TypeScript directly, but behind the scenes it compiles the JavaScript entirely in memory — it’s still a compilation.
The internal process of the compiler
- Reading file: the compiler reads the
.tsfile - Validation: ensures that the code is valid according to TypeScript rules
- Syntax tree translation (AST — Abstract Syntax Tree): the code now exists in a language-independent form
- Emission of JavaScript: the syntax tree is translated into JavaScript
The syntax tree is a language-agnostic representation of logic. Theoretically, one could write a translator to transform the syntax tree into virtually anything. For example, there is ts2python, a limited TypeScript → Python translator.
The TypeScript Compiler API
The TypeScript compiler exposes an API that allows you to access the syntax tree and perform operations like:
- Static analysis of TypeScript code
- Application of custom rules (example: impose a single class per file)
This is beyond the scope of this course, but it is a powerful ability to know.
4.2 Demo: A Closer Look at Transpilation
The Person example class
class Person {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
get name(): string {
return `${this.firstName} ${this.lastName}`;
}
set name(value: string) {
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
static create(firstName: string, lastName: string): Person {
return new Person(firstName, lastName);
}
}
This class contains exactly one of each major element type: two properties of different types, a constructor, getters/setters, and a static method.
The TypeScript AST Viewer
By pasting this code into the TypeScript AST Viewer, we obtain a visual representation of the syntax tree. By collapsing elements, each collapsed section represents a block of code in the editor. By clicking on an element of the AST, the corresponding code is highlighted.
Example of exploration:
- Click on
BinaryExpression→ identifies an assignment expression PropertyAccessExpression→ identifies access to a propertyIdentifyforvaluein the setter → this is the parameter passed to the setter- In the right panel, we see:
type: string— TypeScript correctly identified the type
In the lower left corner: factory.createIdentifier(...) — calls to the factory API that the compiler would use to generate this node.
The emitted JavaScript is surprisingly close to the source TypeScript for simple code — which illustrates that TypeScript is not a heavy overlay.
4.3 Configure your project with tsconfig files
The trainer presents his 8 best tsconfig options (according to him):
1. target — JavaScript target
Sets the target JavaScript version for emitted JavaScript. The default value generated by Visual Studio is ES2016 — very conservative.
- ES2016 was released almost 9 years ago at the time of training; virtually all modern environments support it
- W3Schools lists support on all major browsers since August 2016
Practical impact: If using an ES2017 feature like padStart, the TypeScript compiler will fail with an ES2016 target (or a linter will display a warning).
Current values: "ES5", "ES6"/"ES2015", "ES2016", "ES2017", "ESNext"
2. module — Module format
Defines which code will be issued to make the code available as a module.
The default in Visual Studio is CommonJS. According to typescriptlang.org:
These options are no longer recommended for new projects and will not be covered in detail in this documentation.
CommonJS is from 2009. Comparing code transpiled with CommonJS vs newer targets, we see a lot more shimming and translation code with CommonJS.
Recommendations:
- For new projects:
"esnext" - For Node.js:
"nodenext"
3. allowJs — Allow JavaScript
Allows you to include .js files in a TypeScript project. Useful during a gradual migration.
4. sourceMap / inlineSourceMap — Source maps
Generate source maps for debugging. Mutually exclusive (seen in module 1).
5. removeComments — Remove comments
Remove comments from emitted JavaScript. Reduces the size of transmitted files.
6. newLine — End of line
Controls the type of line ending in emitted JavaScript (CRLF or LF). Useful in multi-platform teams.
7. forceConsistentCasingInFileNames — Consistent case
Forces consistent case in file names referenced in imports. Avoids issues on case-insensitive (Windows) vs. sensitive (Linux/macOS) file systems.
8. strict — Strict mode
Enables all strict type checks. Most important option for clean TypeScript code.
4.4 Demo: Working with tsconfig files
Structure of the tsconfig file
The tsconfig generated by tsc --init contains many commented sections.
Scope: the tsconfig applies to the folder that contains it. This scope can be changed with certain options.
Section Projects
Options to configure how the TypeScript project will be interpreted by the compiler. Includes incremental option for incremental builds — useful for very large TypeScript codebases to speed up compilation. Not to be touched in general unless you know why.
Section Language and Environment
Contains the JSX option for compiling JSX (JavaScript XML — used in particular in React). Related options:
jsx: JSX compilation target (react,react-native,preserve, etc.)reactNamespace: namespace for React emission
Modules Section
All of these options control interoperability between the code and other modules.
module: seen previouslymoduleResolution: how TypeScript resolves importsallowJs: allows.jsfilesbaseUrl/paths: aliases for imports
Important note: at the level of complexity covered in this course, CommonJS and nodenext produce the same output. If we change the value and we don’t see any difference in the .js emitted, this is normal.
JavaScript Support Section
Configuration for JavaScript support in a TypeScript project.
Section Emit
Checks the details of the JavaScript emitted:
sourceMap/inlineSourceMapremoveCommentsnewLineoutDir: specifies the output directory for emitted files
Interop Constraints Section
Controls how libraries are consumed in TypeScript:
esModuleInterop: better compatibility with CommonJS modulesforceConsistentCasingInFileNames
Section Type Checking
strict: enable all strict checks
Section Completeness
skipLibCheck: skips type checking in.d.tsfiles (speeds up compilation)
include / exclude options
Allows you to precisely control which files are included in the compilation:
{
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
4.5 Use type declaration files
The challenge of third-party JavaScript libraries
Common objection:
“That’s all good, but 90% of the code I want to migrate to TypeScript targets untyped third-party JavaScript modules. I’m definitely not going to rewrite them in TypeScript, so migrating won’t do much good since TypeScript can’t check types in that library. »
Two definitions needed for a piece of code
- The execution code (runtime code): necessary for the code to execute
- The type definition (type definition): necessary for designing and compiling for TypeScript
If the code is TypeScript with strict typing, we obtain both from the same source. In the case of third-party JavaScript libraries, you are only guaranteed to have the execution code.
The good news: many libraries already include type declarations
Many large JavaScript libraries provide type declarations with their packages. This means that even if the JavaScript executing an add function uses the any type internally, the developer can communicate that the expected type is number via the type declaration file.
This is especially true as these large libraries themselves migrate to TypeScript in the background. Generating a type declaration file is simple since the definition already exists in the syntax tree.
DefinitelyTyped
If the library does not provide its own type declarations, there is a good chance that they are available elsewhere. Just as npm is the central repository for JavaScript, DefinitelyTyped is a GitHub repository of type definitions for JavaScript packages.
npm install --save-dev @types/react
# ou
npm install --save-dev @types/lodash
# etc.
All DefinitelyTyped content ends up in npm via the @types/ namespace.
4.6 Demo: Type inference from declaration files
The Employee example class
class Employee {
firstName: string;
lastName: string;
employeeId: string;
constructor(firstName: string, lastName: string, employeeId: string) {
this.firstName = firstName;
this.lastName = lastName;
this.employeeId = employeeId;
}
get friendlyName(): string {
return `${this.firstName} ${this.lastName}`;
}
// D'autres éléments en dehors de la classe...
}
Generate a .d.ts file from the command line
tsc --declaration employees.ts
Result: a new employees.d.ts file is created. The .d.ts extension is the standard extension for type declaration files.
Contents of a .d.ts file
// employees.d.ts (généré automatiquement)
declare class Employee {
firstName: string;
lastName: string;
employeeId: string;
constructor(firstName: string, lastName: string, employeeId: string);
get friendlyName(): string;
}
This is a simple declaration of the class interface — no implementation, just the signatures.
Configure automatic transmission via tsconfig
In real life, we configure automatic transmission in tsconfig rather than doing it manually. In the Emit section of tsconfig:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true // Source maps pour les déclarations de types (optionnel)
}
}
The declarationMap option generates source maps for type declarations, allowing library users to debug types.
Write type declarations for an untyped library
For a JavaScript library that has no types and is not in DefinitelyTyped, you can write its own .d.ts file:
// myLibrary.d.ts
declare module "myLibrary" {
function doSomething(input: string): number;
function doSomethingElse(data: any[]): void;
// ...
}
This allows TypeScript to check types when using this library, even if the execution code is untyped JavaScript.
5. Migrate a JavaScript application to TypeScript
5.1 Observer and Pub-Sub patterns
Gang of Four (GoF) patterns
The Gang of Four patterns owe their name to the four authors who created them:
- Erich Gamma
- Richard Helm
- Ralph Johnson
- John Vlissides
Patterns are best described as tactics — they reflect problems we see again and again in software development. Just as a general should not arrive on a battlefield planning to use a particular tactic in advance, we should not choose a pattern and forcefully insert our code into it. In both cases, one must observe the circumstances and make an informed decision on which tactic applies to the situation.
The Observer pattern
With the Observer pattern:
- There is a *subject that maintains a list of objects that depend on it: the observers
- When the subject’s state changes, all observers are notified and can react accordingly
The Pub-Sub pattern
With the pattern Pub-Sub (Publish-Subscribe):
- subscribers are listening to a channel
- They receive notifications when changes are published via a message broker
The hybrid approach for this project
The application uses a hybrid approach that is neither exactly Observer nor exactly Pub-Sub, but which takes advantage of the decoupled communications at the heart of both patterns.
5.2 Our client JavaScript application
Application architecture
The JavaScript client application consumes data from a Node server, representing data from an IoT network. The connection is made via WebSockets.
Once data is transmitted from the server, there are three vectors to process this data:
- Drawing a graph via D3
- Writing to an HTML table
- Logging to console
Pause functionality
The application can currently be paused via a checkbox — but this pauses the entire stream. By migrating to a subscriber model, we could control each vector independently:
- Disable logging until there is a problem
- Stop chart to examine raw data
- Maintain global pause functionality if desired
The Observer base class
The Observer base class will be extremely simple:
abstract class Observer {
abstract receiveNotification(data: dataPacket): void;
}
- Only one method:
receiveNotification - Receives a
dataPacketobject abstractclass: no direct instantiation
The three concrete observers
class ChartObserver extends Observer { /* ... */ }
class TableObserver extends Observer { /* ... */ }
class ConsoleObserver extends Observer { /* ... */ }
Everyone receives the same data but acts differently depending on the problem they are solving.
This architecture makes it very easy to add new vectors later. For example, a LocalStorageObserver which would write data to the browser’s localStorage as it arrives.
Refactoring note
The author emphatically emphasizes that this is refactoring: we start with a system that works, and we end with a system that works in a similar way - but with improved quality measures. The quality targeted here is the structure and readability of the code, not just the correctness.
5.3 Demo: Migrate client JS to TypeScript
Project structure
projet/
├── client/ # Code client (updatechart.js)
│ └── observer.ts # Nouveau fichier à créer
├── tempserver/ # Serveur Node (temp.js)
└── shared/ # Code partagé
└── dataPacket.ts # Classe partagée (déjà en TypeScript)
updatechart.js: client codetemp.js: server (temperature, not temporary) — simulates IoT datadataPacket: shared class withsimulatePacketmethod (simulates data because we do not have a real IoT thermometer)
The server is simply running with Node in a command window. The client is a simple HTML page (not even a web server — opened directly from the file system).
Creating observer.ts
Step 1: Empty class
class Observer {
}
Step 2: Add method
class Observer {
receiveNotification(data: dataPacket);
}
TypeScript immediately complains:
- No implementation for method
dataPacketis not a recognized type
Step 3: Import dataPacket
Copy the require from temp.js and add it, with the appropriate namespace. If it doesn’t work yet, it’s a namespace issue in the modules — resolve by adjusting the import paths.
Step 4: Fix the implementation problem
Make the class and method abstract:
abstract class Observer {
abstract receiveNotification(data: dataPacket): void;
}
Why
abstract classrather than aninterface? An abstract class allows you to create shared logic — for example logging or common validations — which can be inherited by subclasses. An interface cannot contain logic.
5.4 Demo: Migrate Client JS to TypeScript, Part 2
Types for D3
D3 uses generic TypeScript types for visualization components:
// Échelles linéaires : type d'entrée = number, type de sortie = number
let xScale: ScaleLinear<number, number>;
let yScale: ScaleLinear<number, number>;
// Échelles pour les axes
let xAxis: AxisScale<number>;
let yAxis: AxisScale<number>;
// Éléments HTML SVG — type any (on s'intéresse au comportement, pas au type HTML)
let svg: any;
let g: any;
// Tableau de paquets de données
let allData: Array<dataPacket> = []; // Initialisé pour éviter null
Important points:
ScaleLinear<number, number>: two generic arguments — input type and output typeAxisScale<number>: a generic argument — the type of the valuesvgandgtoany: they manipulate HTML elements, this is not the center of interestallDatainitialized immediately (not just declared): ensures that the array is nevernullwhen we start pushing values into it
To make @types/node available: npm install --save-dev @types/node
Class lifetime
This class will be *long-lived: it starts when the browser loads and lasts as long as the browser instance. The initialization code (creating the SVG components, setting up the scales, etc.) is only run once in the constructor — just like the code outside the message event handler in updateChart.js.
Migrating the updateChart.js code
class ChartObserver extends Observer {
private xScale: ScaleLinear<number, number>;
private yScale: ScaleLinear<number, number>;
private allData: Array<dataPacket>;
private chartId: string; // Paramètre pour flexibilité
constructor(chartId: string) {
super(); // Appel obligatoire au constructeur parent
this.chartId = chartId;
this.allData = []; // Initialisation concrète
// Tout le code de updateChart.js en dehors du message event...
// Remplacer les références directes par this.xxx
}
receiveNotification(data: dataPacket): void {
// Code du gestionnaire d'événements message...
}
}
Key transformations:
- All code outside event handler → in constructor
- All code in event handler → in
receiveNotification - Replace direct references with
this.xxx - Add
super()to start of constructor
Compilation check
Compile as a sanity check before connecting to the subscriber model. If it compiles, the logic is structurally correct.
5.5 Demo: Migrate server JS to TypeScript
The server code temp.js
The server is surprisingly simple: only 19 lines of code.
// temp.js original
const WebSocket = require('ws');
const dataPacket = require('./shared/dataPacket');
const wss = new WebSocket.Server({ port: 2112 });
wss.on('connection', function connection(ws) {
console.log('Client connected');
const interval = setInterval(() => {
const packet = new dataPacket();
packet.simulatePacket();
ws.send(JSON.stringify(packet));
}, 1000);
ws.on('close', function() {
console.log('Client disconnected');
clearInterval(interval);
});
});
Creating temp.ts
- Create
temp.ts - Copy/paste the code from
temp.js - Change the
requireWebSocket toimport:
// Avant (CommonJS require)
const WebSocket = require('ws');
// Après (ES module import)
import WebSocket from 'ws';
tsconfig configuration for server
{
"compilerOptions": {
"target": "ES6",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"], // Définitions de types Node.js (optionnel mais utile)
"strict": true
}
}
Key points of this configuration:
- target ES6: Modern Node.js supports ES6
- module/moduleResolution NodeNext: suitable for Node.js
- node types: can be omitted, but useful if you want to interact with Node APIs
- strict: enable all strict TypeScript checks
Compiling and testing
tsc --project tsconfig.json
Result: Node.js code for modules is generated at the top of the file; the business code is almost entirely unchanged.
node temp.js
No error = success. We refresh the browser and everything works.
Final assessment and conclusion
The Observer pattern is now implemented. The next logical steps would be:
- Break down
ChartObserverfurther to make it more fluent - Make it more testable: with a little more refactoring, we could send a known set of packets against the chart listener, capture the correct state of the SVG object, and test against that known state in the future
Can we do all this in JavaScript?
Yes, of course. We can create abstract classes, superclasses, subclasses, static methods in pure JavaScript. But the fundamental truth is:
In TypeScript, these ideas are MUCH clearer than approximating what an abstract class means in pure JavaScript.
That’s what TypeScript is all about: taking things you can do in JavaScript, but making them obvious in the code in a way that any developer familiar with the large C language family (C#, Java, C++, etc.) will find understandable.
6. Reference links
- ts2python (limited TypeScript → Python translator): https://pypi.org/project/ts2python
- Introduction to TypeScript Compiler API: https://writer.sh/posts/gentle-introduction-to-typescript-compiler-api
- ts-factory-code-generator-generator: https://github.com/dsherret/ts-factory-code-generator-generator
- DefinitelyTyped (type definition repository): https://github.com/DefinitelyTyped
Search Terms
typescript · foundations · essentials · javascript · developers · react · frontend · development · type · tsconfig · types · class · compilation · application · inference · interface · migrate · typing · client · compiler · concrete · generics · observer · options