2. Typage structurel
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Code : les fichiers examples/l02_* et errors/l02_*, et les côtés C# et Java dans compare/l02_nullable.cs, compare/L02Nullable.java, compare_fail/l02_nominal.cs et compare_fail/L02Nominal.java.
Les exemples affichent leurs résultats avec les fonctions utilitaires show et attempt de la leçon 2 de JavaScript, désormais typées. err dans un catch a le type unknown, parce que JavaScript peut lever n’importe quelle valeur, et la fonction vérifie que c’est une Error avant de lire son message :
import { inspect } from 'node:util';
export function show(label: string, value: unknown): void { console.log(`${label.padEnd(34)} ${inspect(value)}`);}
export function attempt(label: string, fn: () => unknown): void { try { show(label, fn()); } catch (err) { // err est unknown : on peut lever n'importe quoi, pas seulement une Error (la leçon 3 le restreint) console.log(`${label.padEnd(34)} ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`); }}Annotations et inférence
Section intitulée « Annotations et inférence »Une annotation, c’est un deux-points et un type après un nom : let count: number. La plupart du temps, tu n’en écris pas, parce que tsc infère le type à partir de la valeur, comme le fait var en C# et en Java. Le moyen le plus rapide de voir ce que tsc a inféré, hors d’un éditeur, est de lui demander un fichier de déclaration : tsc --declaration écrit le type de tout ce qu’un module exporte.
// tsc --declaration écrit les types qu'il a inférés dans l02_inference.d.ts (voir check.sh)export let count = 1;export const tuning = 'EADGBE';export const strings = ['E', 'A', 'D', 'G', 'B', 'E'];export const capo = { fret: 2, label: 'capo' };export const frozen = Object.freeze({ fret: 2, label: 'capo' });export const literal = { fret: 2, label: 'capo' } as const;export const mixed = [1, 'two', null];export const pair: [string, number] = ['capo', 2];export function fretOf(label: string) { return label === 'capo' ? capo.fret : undefined;}export const parsed = JSON.parse('{"fret": 2}');// out/dts/examples/l02_inference.d.ts, écrit par tsc --declaration --emitDeclarationOnlyexport declare let count: number;export declare const tuning = "EADGBE";export declare const strings: string[];export declare const capo: { fret: number; label: string;};export declare const frozen: Readonly<{ fret: 2; label: "capo";}>;export declare const literal: { readonly fret: 2; readonly label: 'capo';};export declare const mixed: (string | number | null)[];export declare const pair: [string, number];export declare function fretOf(label: string): number | undefined;export declare const parsed: any;Une partie correspond à ce que C# inférerait, une autre non :
countest unnumber, maistuninga le type"EADGBE": un type littéral, dont la seule valeur est cette chaîne. Unconstne peut pas changer, donctscgarde le type le plus étroit. Unletreçoit le type plus largenumberoustring, puisqu’il peut être réassigné.- Les propriétés de
caposont élargies ennumberetstring, parce que les propriétés d’un objet peuvent être réassignées même quand la variable estconst.Object.freezeetas constgardent les types littéraux, et ajoutentreadonly. mixedest un tableau destring | number | null: une union des types des éléments, là où C# refusenew[] { 1, "two", null }faute de meilleur type commun (à vérifier). Les unions sont le sujet de la leçon 3.fretOfa un type de retour inféré,number | undefined, puisqu’une branche renvoieundefined.parsedestany, parce queJSON.parseest déclaré comme renvoyantany. La section surany, plus bas, explique pourquoi c’est important.
Le style habituel consiste à annoter ce dont dépend le reste du code, les paramètres et les types de retour des fonctions exportées, et à laisser tsc inférer les variables locales. Les paramètres doivent être annotés de toute façon : tsc n’infère pas le type d’un paramètre à partir des appels.
| C# | Java | TypeScript | |
|---|---|---|---|
| Nombres | int, long, double, decimal… |
int, long, double… |
number, bigint |
| Texte | string, char |
String, char |
string, pas de type caractère |
| Valeur absente | null |
null |
null et undefined, deux types |
| N’importe quelle valeur, vérifiée avant usage | object |
Object |
unknown |
| N’importe quelle valeur, sans vérification | dynamic |
— | any |
| Aucune valeur | void pour un type de retour |
void |
void pour un type de retour, never pour une valeur qui ne peut pas exister |
| Une valeur précise | — | — | un type littéral : 'EADGBE', 2, true |
Typage structurel
Section intitulée « Typage structurel »Les types de C# et de Java sont nominaux : une classe est compatible avec une interface parce qu’elle le déclare, class Vector : IPoint. Les types de TypeScript sont structurels : une valeur est compatible avec un type quand elle a les bonnes propriétés avec les bons types, quoi que dise sa déclaration (compatibilité des types).
import { show } from './show.ts';
interface Point { x: number; y: number;}
function length(p: Point): number { return Math.hypot(p.x, p.y);}
// Une classe qui ne mentionne jamais Point, et dont les instances sont pourtant des Points : seule la forme compteclass Vector { x: number; y: number; z = 0; constructor(x: number, y: number) { this.x = x; this.y = y; }}show('length(new Vector(3, 4))', length(new Vector(3, 4)));show('length({ x: 3, y: 4 })', length({ x: 3, y: 4 }));
// interface et type décrivent la même forme : les deux noms sont interchangeablestype PointAlias = { x: number; y: number };const alias: PointAlias = { x: 6, y: 8 };const point: Point = alias;show('length(point)', length(point));
// Deux classes de même forme sont le même type, quoi que disent leurs nomsclass Celsius { constructor(value: number) { this.value = value; } value: number;}class Fahrenheit { constructor(value: number) { this.value = value; } value: number;}function boils(t: Celsius): boolean { return t.value >= 100;}show('boils(new Fahrenheit(150))', boils(new Fahrenheit(150)));const water: Celsius = new Fahrenheit(212);show('water instanceof Celsius', water instanceof Celsius);
// Un type marqué : un nombre que seule une fonction peut produiretype Kelvin = number & { readonly brand: 'Kelvin' };function kelvin(value: number): Kelvin { if (value < 0) throw new RangeError('below absolute zero'); return value as Kelvin; // le seul endroit où la marque est affirmée}const room = kelvin(293.15);show('room', room);show('room - 273.15', room - 273.15);length(new Vector(3, 4)) 5length({ x: 3, y: 4 }) 5length(point) 10boils(new Fahrenheit(150)) truewater instanceof Celsius falseroom 293.15room - 273.15 20Vector ne mentionne jamais Point, et ses instances sont acceptées là où un Point est attendu, avec la propriété z en plus. C’est ce qui permet à TypeScript de coller à JavaScript, où la plupart des objets sont des littéraux sans classe. Le prix à payer est la seconde moitié de l’exemple : Celsius et Fahrenheit ont la même forme, donc ils sont le même type pour tsc, et une température de 150 °F est déclarée bouillante. instanceof les distingue toujours à l’exécution, parce que la chaîne de prototypes est réelle, mais les types, eux, ne les distinguent pas. Le même code en C# et en Java :
> dotnet run l02_nominal.cscompare_fail/l02_nominal.cs(2,17): error CS0029: Cannot implicitly convert type 'Fahrenheit' to 'Celsius'
The build failed. Fix the build errors and run again.> javac L02Nominal.javaL02Nominal.java:7: error: incompatible types: Fahrenheit cannot be converted to Celsius Celsius water = new Fahrenheit(212); ^1 errorQuand un type a besoin d’un nom, deux techniques lui en donnent un. Un type marqué (branded type) est l’intersection d’un type primitif et d’une propriété qu’aucune valeur ordinaire ne possède, number & { readonly brand: 'Kelvin' }, de sorte que seule une fonction qui affirme la marque en produit un ; à l’exécution, room est un simple nombre. Et un champ privé #value rend une classe nominale, parce qu’aucune autre classe ne peut avoir ce champ :
// Un champ #private rend une classe nominale : aucune autre classe, si semblable soit-elle, n'a ce champclass Celsius { #value: number; constructor(value: number) { this.#value = value; } get value() { return this.#value; }}class Fahrenheit { #value: number; constructor(value: number) { this.#value = value; } get value() { return this.#value; }}
type Kelvin = number & { readonly brand: 'Kelvin' };
const water: Celsius = new Fahrenheit(212);const room: Kelvin = 293.15;console.log(water.value, room);> npx tsc -p out/tsconfig.l02_nominal.json --prettyerrors/l02_nominal.ts:24:7 - error TS2322: Type 'Fahrenheit' is not assignable to type 'Celsius'. Property '#value' in type 'Fahrenheit' refers to a different member that cannot be accessed from within type 'Celsius'.
24 const water: Celsius = new Fahrenheit(212); ~~~~~
errors/l02_nominal.ts:25:7 - error TS2322: Type 'number' is not assignable to type 'Kelvin'. Type 'number' is not assignable to type '{ readonly brand: "Kelvin"; }'.
25 const room: Kelvin = 293.15; ~~~~
Found 2 errors in the same file, starting at: errors/l02_nominal.ts:24Le mot-clé private propre à TypeScript a le même effet sur la compatibilité, vérifié par tsc seulement ; #value est aussi imposé par le moteur (leçon 4 de JavaScript).
interface ou type
Section intitulée « interface ou type »interface Point { x: number; y: number } et type Point = { x: number; y: number } décrivent la même forme, et l’exemple assigne l’un à l’autre sans broncher. Les différences sont ailleurs (types de tous les jours) :
interface |
type |
|
|---|---|---|
| Formes d’objets | oui | oui |
| Unions, tuples, primitifs, types mappés | non | oui : type Fret = number | 'open' |
| Extension | interface Guitar extends Instrument |
une intersection : type Guitar = Instrument & { strings: number } |
| Deux déclarations du même nom | fusionnées en une seule interface | une erreur |
La fusion de déclarations est ce qui permet aux bibliothèques d’ajouter une propriété à un type global, et c’est rarement ce que veut une application. Une règle raisonnable, et celle que suit ce cours : interface pour les formes d’objets, type pour tout le reste.
Vérification des propriétés en trop
Section intitulée « Vérification des propriétés en trop »Le typage structurel accepte les propriétés en trop, à une exception près :
interface SceneOptions { stars?: boolean; tower?: boolean; skyboxMode?: string;}
function describe(options: SceneOptions): string { return `stars ${options.stars ?? true}, tower ${options.tower ?? false}`;}
// Un littéral objet écrit là où un SceneOptions est attendu : une propriété inconnue est une erreurconsole.log(describe({ stars: false, towr: true }));
// Le même objet d'abord placé dans une variable : pas de vérification des propriétés en trop, et la faute de frappe est ignorée en silenceconst fromUrl = { stars: false, towr: true };console.log(describe(fromUrl));> npx tsc -p out/tsconfig.l02_excess.json --prettyerrors/l02_excess.ts:13:38 - error TS2561: Object literal may only specify known properties, but 'towr' does not exist in type 'SceneOptions'. Did you mean to write 'tower'?
13 console.log(describe({ stars: false, towr: true })); ~~~~
Found 1 error in errors/l02_excess.ts:13> node errors/l02_excess.tsstars false, tower falsestars false, tower falseUn littéral objet écrit directement là où un type est attendu est « frais », et tsc y cherche les propriétés que le type n’a pas, puisque rien d’autre ne pourra jamais les lire. Le même objet stocké d’abord dans une variable n’est plus frais : il pourrait servir ailleurs, là où towr a un sens, donc tsc l’accepte, et la faute de frappe se perd sans message. Avec des propriétés toutes optionnelles, comme dans un objet d’options, c’est le cas où une faute de frappe coûte le plus cher. Passe les options sous forme de littéraux, ou annote la variable, const fromUrl: SceneOptions = { … }, ce qui rend le littéral à nouveau frais.
readonly
Section intitulée « readonly »interface Tuning { readonly name: string; readonly notes: readonly string[];}
const standard: Tuning = { name: 'standard', notes: ['E', 'A', 'D', 'G', 'B', 'E'] };standard.name = 'drop D';standard.notes[0] = 'D';standard.notes.push('A');console.log(standard);> npx tsc -p out/tsconfig.l02_readonly.json --prettyerrors/l02_readonly.ts:8:10 - error TS2540: Cannot assign to 'name' because it is a read-only property.
8 standard.name = 'drop D'; ~~~~
errors/l02_readonly.ts:9:1 - error TS2542: Index signature in type 'readonly string[]' only permits reading.
9 standard.notes[0] = 'D'; ~~~~~~~~~~~~~~~~~
errors/l02_readonly.ts:10:16 - error TS2339: Property 'push' does not exist on type 'readonly string[]'.
10 standard.notes.push('A'); ~~~~
Found 3 errors in the same file, starting at: errors/l02_readonly.ts:8> node errors/l02_readonly.ts{ name: 'drop D', notes: [ 'D', 'A', 'D', 'G', 'B', 'E', 'A' ]}readonly sur une propriété, et readonly string[] pour un tableau, retirent les écritures du type, comme le fait IReadOnlyList<T> en C# : un readonly string[] n’a pas de push. Et comme avec IReadOnlyList<T>, c’est une vue, pas un objet immuable :
import { attempt, show } from './show.ts';
interface Tuning { readonly name: string; readonly notes: readonly string[];}
// readonly n'est vérifié que par tsc : rien n'est gelé à l'exécutionconst standard: Tuning = { name: 'standard', notes: ['E', 'A', 'D', 'G', 'B', 'E'] };
// Un type readonly est assignable à un type mutable qui a les mêmes propriétés : l'alias peut écrireconst writable: { name: string } = standard;writable.name = 'drop D';show('standard.name', standard.name);
// Object.freeze donne les deux : un Readonly<T> pour tsc, et un objet gelé pour le moteurconst frozen = Object.freeze({ name: 'open G', notes: ['D', 'G', 'D', 'G', 'B', 'D'] });attempt("frozen.name = 'x', after a cast", () => { (frozen as { name: string }).name = 'x';});frozen.notes.push('shallow'); // freeze est superficiel, et Readonly<T> aussishow('frozen.notes.length', frozen.notes.length);
// as const : les types littéraux les plus étroits, et readonly à tous les niveauxconst modes = ['ionian', 'dorian', 'phrygian'] as const;type Mode = (typeof modes)[number];const mode: Mode = 'dorian';show('modes.includes(mode)', modes.includes(mode));standard.name 'drop D'frozen.name = 'x', after a cast TypeError: Cannot assign to read only property 'name' of object '#<Object>'frozen.notes.length 7modes.includes(mode) true- Rien n’est gelé à l’exécution :
readonlydisparaît avec les autres types, et l’exécution parnodeci-dessus a tout modifié. - Un type readonly est assignable au même type sans
readonly, donc un alias peut écrire ce que l’original ne pouvait pas.tscaccepteconst writable: { name: string } = standard, etstandard.namechange. Object.freezedonne les deux moitiés : son type de retour estReadonly<…>, et le moteur refuse l’écriture, ici faite à travers une assertion de type qui fait tairetsc. Les deux sont superficiels, comme en JavaScript.as constrend un littéral readonly à tous les niveaux, avec des types littéraux.(typeof modes)[number]transforme les éléments du tableau en type union,'ionian' | 'dorian' | 'phrygian', le remplaçant d’unenumqu’utilisait le deuxième exercice de la leçon 1.
Tuples et index
Section intitulée « Tuples et index »import { show } from './show.ts';
// Un tuple : une longueur fixe, et un type pour chaque positiontype Interval = [name: string, semitones: number];const fifth: Interval = ['perfect fifth', 7];const [name, semitones] = fifth;show('name, semitones', [name, semitones]);
// Un tableau : une longueur quelconque, un seul type d'élément, et un index auquel tsc fait confianceconst strings: string[] = ['E', 'A', 'D', 'G', 'B', 'E'];const seventh: string = strings[6]; // pas d'erreur sans noUncheckedIndexedAccessshow('seventh', seventh);show('typeof seventh', typeof seventh);
const counts = new Map<string, number>([['E', 2]]);const count = counts.get('A'); // Map.get l'admet : number | undefinedshow('count ?? 0', count ?? 0);
// Les tuples sont des tableaux à l'exécution : rien n'empêche un pushfifth.push('extra');show('fifth', fifth);name, semitones [ 'perfect fifth', 7 ]seventh undefinedtypeof seventh 'undefined'count ?? 0 0fifth [ 'perfect fifth', 7, 'extra' ]Un type tuple fixe la longueur et le type de chaque position, et ses étiquettes, name et semitones, servent de documentation. À l’exécution, c’est un tableau, et push fonctionne : le type ne protège que les positions qu’il déclare.
strings[6] est typé string, et contient undefined. Par défaut, tsc fait confiance à un index dans un tableau ou un dictionnaire, là où C# lèverait IndexOutOfRangeException pour un tableau et KeyNotFoundException pour un dictionnaire, et Java ArrayIndexOutOfBoundsException. Map.get est déclaré honnêtement, number | undefined, mais un index de tableau ou une clé de Record ne l’est pas. L’option noUncheckedIndexedAccess ajoute undefined à chaque accès par index. Elle ne fait pas partie de strict, et tsc --init l’active :
// options de tsc : --noUncheckedIndexedAccessconst strings: string[] = ['E', 'A', 'D', 'G', 'B', 'E'];const seventh: string = strings[6];const record: Record<string, number> = { E: 2 };const count: number = record['A'];console.log(seventh.toLowerCase(), count + 1);> npx tsc -p out/tsconfig.l02_index.json --pretty> npx tsc -p out/tsconfig.l02_index.json --pretty --noUncheckedIndexedAccesserrors/l02_index.ts:4:7 - error TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.
4 const seventh: string = strings[6]; ~~~~~~~
errors/l02_index.ts:6:7 - error TS2322: Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.
6 const count: number = record['A']; ~~~~~
Found 2 errors in the same file, starting at: errors/l02_index.ts:4> node errors/l02_index.tserrors/l02_index.ts:7console.log(seventh.toLowerCase(), count + 1); ^
TypeError: Cannot read properties of undefined (reading 'toLowerCase')
Node.js v24.21.0Sans l’option, tsc n’affiche rien et le programme échoue à la première ligne qui utilise l’élément manquant. Avec elle, chaque index demande une vérification, ce qui est bruyant dans les boucles sur des index connus, et juste pour les recherches par clé.
any, unknown et never
Section intitulée « any, unknown et never »import { attempt, show } from './show.ts';
const saved = '{"stars": "yes", "tower": true}';
// JSON.parse renvoie any : tout usage compile, et any se propage à tout ce qu'il toucheconst options = JSON.parse(saved);const stars: boolean = options.stars; // pas d'erreur : any est assignable à toutshow('stars', stars);show('typeof stars', typeof stars);attempt('options.weather.level', () => options.weather.level);
// unknown accepte lui aussi n'importe quelle valeur, mais on ne peut rien en faire avant une vérificationconst checked: unknown = JSON.parse(saved);if (typeof checked === 'object' && checked !== null && 'stars' in checked) { show("typeof checked.stars", typeof checked.stars);}
// never : une fonction qui ne revient pas, et une valeur qui ne peut pas existerfunction fail(message: string): never { throw new Error(message);}function starsOf(value: unknown): boolean { return typeof value === 'boolean' ? value : fail(`not a boolean: ${JSON.stringify(value)}`);}attempt('starsOf(options.stars)', () => starsOf(options.stars));stars 'yes'typeof stars 'string'options.weather.level TypeError: Cannot read properties of undefined (reading 'level')typeof checked.stars 'string'starsOf(options.stars) Error: not a boolean: "yes"any désactive le vérificateur pour une valeur. options est any parce que JSON.parse renvoie any, donc options.stars est any aussi, et any est assignable à tous les types : stars est déclaré boolean et contient la chaîne 'yes', et options.weather.level compile et lève une exception. any se comporte comme le dynamic de C#, à une différence près : C# lie une opération dynamic au moment où elle s’exécute et lève RuntimeBinderException pour un membre qui n’existe pas, là où JavaScript lit undefined et continue. Il se propage en silence, depuis les fonctions qui le renvoient, JSON.parse, response.json() et les parties non typées des bibliothèques, à tout ce qu’il touche.
unknown accepte lui aussi toutes les valeurs, et ne permet rien tant qu’une vérification ne l’a pas restreint, comme object en C# :
const options: unknown = JSON.parse('{"stars": "yes"}');const stars: boolean = options.stars;const copy: boolean = options;
console.log(stars, copy);> npx tsc -p out/tsconfig.l02_unknown.json --prettyerrors/l02_unknown.ts:3:24 - error TS18046: 'options' is of type 'unknown'.
3 const stars: boolean = options.stars; ~~~~~~~
errors/l02_unknown.ts:4:7 - error TS2322: Type 'unknown' is not assignable to type 'boolean'.
4 const copy: boolean = options; ~~~~
Found 2 errors in the same file, starting at: errors/l02_unknown.ts:3Dans l’exemple, typeof checked === 'object', !== null et 'stars' in checked restreignent checked étape par étape, jusqu’à ce que checked.stars compile. La leçon 3 porte sur ces vérifications. Écris const value: unknown = JSON.parse(text) : l’annotation transforme le any en unknown sur-le-champ, et tsc demande ensuite une vérification avant chaque usage.
never est le type qui n’a aucune valeur. Une fonction qui lève toujours une exception renvoie never, comme une méthode C# marquée [DoesNotReturn], et un never est assignable à tous les types, c’est pourquoi fail(…) trouve sa place dans la branche boolean de starsOf. La leçon 3 utilise never pour vérifier qu’un switch traite tous les cas.
null et undefined
Section intitulée « null et undefined »Avec strictNullChecks, qui fait partie de strict, null et undefined sont des types séparés, et string ne les inclut pas. Une valeur qui peut être absente le dit, string | undefined, ou name?: string pour un paramètre ou une propriété optionnels :
function initial(name?: string): string { return name.charAt(0);}
const tunings = new Map([['standard', 'EADGBE']]);const dropD: string = tunings.get('drop D');
let capo: number = null;console.log(initial(), dropD.length, capo);> npx tsc -p out/tsconfig.l02_null.json --prettyerrors/l02_null.ts:3:10 - error TS18048: 'name' is possibly 'undefined'.
3 return name.charAt(0); ~~~~
errors/l02_null.ts:7:7 - error TS2322: Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.
7 const dropD: string = tunings.get('drop D'); ~~~~~
errors/l02_null.ts:9:5 - error TS2322: Type 'null' is not assignable to type 'number'.
9 let capo: number = null; ~~~~
Found 3 errors in the same file, starting at: errors/l02_null.ts:3> node errors/l02_null.tserrors/l02_null.ts:3 return name.charAt(0); ^
TypeError: Cannot read properties of undefined (reading 'charAt')
Node.js v24.21.0La correction passe par le narrowing de la leçon 3, ou par les opérateurs que la leçon 2 de JavaScript a présentés :
import { attempt, show } from './show.ts';
function initial(name?: string): string { return name === undefined ? '?' : name.charAt(0); // restreint : name est une string dans la seconde branche}show('initial()', initial());show("initial('Ada')", initial('Ada'));
const tunings = new Map([['standard', 'EADGBE']]);show("tunings.get('drop D')?.length", tunings.get('drop D')?.length);show("tunings.get('drop D') ?? 'DADGBE'", tunings.get('drop D') ?? 'DADGBE');
// L'assertion non nulle ! fait taire tsc, et ne vérifie rienattempt("tunings.get('drop D')!.length", () => tunings.get('drop D')!.length);initial() '?'initial('Ada') 'A'tunings.get('drop D')?.length undefinedtunings.get('drop D') ?? 'DADGBE' 'DADGBE'tunings.get('drop D')!.length TypeError: Cannot read properties of undefined (reading 'length')Les types référence nullables de C# reposent sur la même idée, avec le même opérateur !, mais ils produisent des avertissements :
> dotnet run l02_nullable.cscompare/l02_nullable.cs(4,33): warning CS8602: Dereference of a possibly null reference.Initial(null): NullReferenceExceptioncertain.Length: NullReferenceException> java L02Nullable.javainitial(dropD): NullPointerExceptionC# avec <Nullable>enable</Nullable> |
Java | TypeScript avec strictNullChecks |
|
|---|---|---|---|
| Un type qui peut être absent | string? |
rien dans le type ; des annotations comme le @Nullable de JSpecify |
string | undefined, string | null, name?: string |
| L’utiliser sans vérification | avertissement CS8602, le programme se compile | rien, jusqu’à NullPointerException |
erreur TS18048, et le programme s’exécute quand même |
| « Fais-moi confiance » | name! |
— | name! |
| À l’exécution | NullReferenceException |
NullPointerException |
TypeError: Cannot read properties of undefined |
En C# comme en TypeScript, ! ne fait que faire taire le compilateur : tunings.get('drop D')!.length compile et lève une exception. Utilise-le là où tu sais quelque chose que le vérificateur ne peut pas savoir, et préfère une vérification qui dit ce qui doit se passer si tu te trompes.
La famille strict
Section intitulée « La famille strict »strict active huit options, listées dans le code source du compilateur comme les options strictFlag :
| Option | Ce qu’elle vérifie | Leçon |
|---|---|---|
strictNullChecks |
null et undefined sont des types séparés |
celle-ci |
noImplicitAny |
un paramètre ou une variable dont le type ne peut pas être inféré doit être annoté | celle-ci |
useUnknownInCatchVariables |
la variable d’un catch est unknown, pas any |
la fonction attempt plus haut |
strictFunctionTypes |
les types de fonctions sont vérifiés de façon contravariante dans leurs paramètres | 4 |
strictBindCallApply |
bind, call et apply vérifient leurs arguments |
— |
strictPropertyInitialization |
un champ de classe doit être initialisé, dans sa déclaration ou dans le constructeur | — |
strictBuiltinIteratorReturn |
la valeur return des itérateurs intégrés est undefined, pas any |
— |
noImplicitThis |
this doit avoir un type connu dans une fonction |
— |
Un projet peut fixer "strict": true et désactiver ensuite l’une d’elles, et c’est là qu’il faut regarder en premier dans un tsconfig.json existant.
Dans GuitarAlchemist/ga : noImplicitAny désactivé
Section intitulée « Dans GuitarAlchemist/ga : noImplicitAny désactivé »ga-react-components/tsconfig.app.json fixe "noImplicitAny": false trois lignes au-dessus de "strict": true. Le fichier est lu comme un tout, donc l’option spécifique l’emporte sur la famille, et chaque paramètre dont le type ne peut pas être inféré devient any en silence. Celui de ga-client n’a pas cette ligne.
J’ai lancé tsc -p tsconfig.app.json --noImplicitAny true sur la bibliothèque de composants, avec TypeScript 5.9.3 et les dépendances résolues à partir de son package.json : cela ajoute 4 erreurs à celles de la leçon 1, deux TS7006, Parameter 'child' implicitly has an 'any' type et la même pour obj, dans BSPDoomExplorer.tsx, lignes 4363 et 4367, et deux TS7053 pour l’indexation d’un Record<HexavalentTruth, string> par une simple string dans IxqlFormPanel.tsx, lignes 83 et 112. Réactiver l’option coûte quatre annotations ; le any que le projet ne voit pas vient d’ailleurs, de 42 appels à JSON.parse et de 18 as any dans ses sources, que la leçon 3 examine.
À retenir
Section intitulée « À retenir »tscinfère les types locaux, y compris des types littéraux pour les constantes ; annote les paramètres et les signatures exportées.tsc --declarationmontre ce qu’il a inféré.- La compatibilité est structurelle : même forme, même type, quels que soient les noms. Les marques et les champs
#privatedonnent un nom à un type quand il en a besoin. - Un littéral objet écrit là où un type est attendu est vérifié pour les propriétés en trop ; le même objet passé par une variable ne l’est pas.
readonlyest une vue à la compilation : un alias sansreadonlypeut toujours écrire, et rien n’est gelé.- Les index de tableaux et de
Recordsont crus sur parole, sauf sinoUncheckedIndexedAccessest activé. anydésactive la vérification et se propage ;unknownexige une vérification ;nevern’a aucune valeur.JSON.parserenvoieany: stocke son résultat dans ununknown.strictNullChecksfait de l’absence une partie du type, comme les types référence nullables de C#, mais sous forme d’erreurs ;!fait taire les deux compilateurs et ne vérifie rien.
Exercices
Section intitulée « Exercices »- Écris des types marqués
CelsiusetFahrenheit, une fonctiontoFahrenheit(t: Celsius): Fahrenheit, etboils(t: Celsius), et montre trois erreurs quetscrejette désormais, dans un fichier quetscaccepte.
Solution
type Celsius = number & { readonly unit: 'Celsius' };type Fahrenheit = number & { readonly unit: 'Fahrenheit' };
function celsius(value: number): Celsius { return value as Celsius;}function fahrenheit(value: number): Fahrenheit { return value as Fahrenheit;}function toFahrenheit(t: Celsius): Fahrenheit { return fahrenheit((t * 9) / 5 + 32);}function boils(t: Celsius): boolean { return t >= 100;}
const water = celsius(100);const oven = fahrenheit(350);console.log(toFahrenheit(water), boils(water));
// Jamais appelée : chaque ligne montre une erreur que tsc rejette désormaisfunction mistakes() { // @ts-expect-error: un Fahrenheit n'est pas un Celsius boils(oven); // @ts-expect-error: un simple nombre n'est pas non plus un Celsius boils(212); // @ts-expect-error: le résultat d'une opération arithmétique redevient un simple nombre const warmer: Celsius = water + 1; return warmer;}console.log(typeof mistakes);212 truefunctionUn commentaire // @ts-expect-error indique à tsc que la ligne suivante doit être une erreur : le fichier compile, et si un changement ultérieur rendait l’une de ces lignes valide, tsc signalerait la directive comme inutilisée. C’est la façon TypeScript de tester que du code est rejeté, là où ce cours utilise des extraits d’erreur séparés. La troisième erreur montre la limite des marques : l’arithmétique sur un Celsius donne un simple number, donc chaque opération qui devrait garder l’unité passe par une fonction.
- Écris
loadTunings(raw: string | null): Tuning[]pour des accordages enregistrés en JSON, qui renvoie seulement les entrées ayant unnamede type chaîne et un tableau de chaînesnotes, et un tableau vide pour tout le reste, JSON invalide compris.
Solution
solutions/l02_ex2_load_tunings.ts :
interface Tuning { name: string; notes: string[];}
// JSON.parse dans un unknown : tsc ne laisse rien passer tant que chaque propriété n'est pas vérifiéefunction loadTunings(raw: string | null): Tuning[] { if (raw === null) return []; let value: unknown; try { value = JSON.parse(raw); } catch { return []; } if (!Array.isArray(value)) return []; const tunings: Tuning[] = []; for (const item of value) { if ( typeof item === 'object' && item !== null && typeof item.name === 'string' && Array.isArray(item.notes) && item.notes.every((note: unknown) => typeof note === 'string') ) { tunings.push({ name: item.name, notes: item.notes }); } } return tunings;}
console.log(loadTunings(null));console.log(loadTunings('{not json'));console.log(loadTunings('{"name": "standard"}'));console.log(loadTunings('[{"name": "drop D", "notes": ["D","A","D","G","B","E"]}, {"name": 7}, {"name": "open", "notes": [1]}]'));[][][][ { name: 'drop D', notes: [ 'D', 'A', 'D', 'G', 'B', 'E' ] } ]Le résultat de JSON.parse va dans un unknown, et la fonction renvoie de nouveaux objets construits uniquement à partir de propriétés vérifiées, donc une propriété en trop dans les données enregistrées n’atteint pas le programme. Regarde pourtant item.name : il a compilé sans vérification 'name' in item, parce que Array.isArray restreint un unknown en any[], et chaque item redevient any. any revient par les déclarations de la bibliothèque standard ; ce sont les vérifications typeof qui rendent cette fonction correcte, pas tsc.
- Pour chaque ligne numérotée de
solutions/l02_ex3_predict.ts, prédis sitscl’accepte, puis retire les commentaires@ts-expect-erroret lancetscpour vérifier.
interface Point { x: number; y: number }interface ReadonlyPoint { readonly x: number; readonly y: number }
const p3 = { x: 1, y: 2, z: 3 };const a: Point = p3; // 1const b: Point = { x: 1, y: 2, z: 3 }; // 2const c: ReadonlyPoint = a; // 3const d: Point = c; // 4const e: [number, number] = [1, 2, 3]; // 5const f: number[] = [1, 2] as [number, number]; // 6const g: string = JSON.parse('"x"') as unknown; // 7const h: string = JSON.parse('1'); // 8const i: number = null; // 9Solution
// Chaque ligne que tsc rejette porte @ts-expect-error : si l'une d'elles compilait, tsc signalerait une directive inutiliséeinterface Point { x: number; y: number;}interface ReadonlyPoint { readonly x: number; readonly y: number;}
const p3 = { x: 1, y: 2, z: 3 };const a: Point = p3; // 1. acceptée : ce n'est pas un littéral frais, et z est en trop// @ts-expect-error 2. rejetée : propriété z en trop dans un littéral objet fraisconst b: Point = { x: 1, y: 2, z: 3 };const c: ReadonlyPoint = a; // 3. acceptée : readonly restreint seulement ce que c peut faireconst d: Point = c; // 4. acceptée : readonly n'affecte pas l'assignabilité// @ts-expect-error 5. rejetée : [number, number] n'a pas de troisième élémentconst e: [number, number] = [1, 2, 3];const f: number[] = [1, 2] as [number, number]; // 6. acceptée : un tuple est un tableau// @ts-expect-error 7. rejetée : un unknown doit être restreint avant d'être assigné à une stringconst g: string = JSON.parse('"x"') as unknown;const h: string = JSON.parse('1'); // 8. acceptée : any est assignable à string, et h contient 1// @ts-expect-error 9. rejetée : null n'est pas un number sous strictNullChecksconst i: number = null;
console.log([a, b, c, d, e, f, g, typeof h, i].length);9Les lignes 2, 5, 7 et 9 sont rejetées : un littéral frais avec une propriété en trop, un tuple de mauvaise longueur, un unknown utilisé comme string, et null sous strictNullChecks. Les lignes 1, 3, 4, 6 et 8 sont acceptées, et deux d’entre elles méritent un second regard : la ligne 4 perd readonly à travers un alias, et la ligne 8 stocke le nombre 1 dans une string, parce que JSON.parse renvoie any.