5. Les objets littéraux
Nous appelons ici « objets littéraux » des objets définis directement dans le code, par opposition aux instances de [class] (vues au chapitre « Les classes »). Les scripts de ce chapitre se trouvent dans le dossier [objets] du projet.
5.1. script [obj-01]
Comme pour les tableaux, un objet est manipulé via un pointeur. En TypeScript, il faut de plus décrire la forme de l'objet avec une interface, avant de pouvoir lui ajouter des propriétés dynamiquement.
| 'use strict';
// un objet vide
// interface : on annonce à l'avance les propriétés qu'on va ajouter dynamiquement,
// car TypeScript (contrairement à JavaScript) exige de connaître la forme d'un objet
interface Obj1 {
prop1?: string;
prop2?: number[];
prop3?: boolean;
[clé: string]: unknown;
}
const obj1: Obj1 = {};
// on peut créer dynamiquement les propriétés de l'objet
obj1.prop1 = "abcd";
console.log('obj1=', obj1);
// autre propriété
obj1.prop2 = [1, 2, 3];
console.log("obj1=", obj1);
// autre propriété avec une notation différente
obj1['prop3'] = true;
console.log("obj1=", obj1);
// obj1 est une référence sur l'objet (pointeur), pas l'objet lui-même
const obj2: Obj1 = obj1;
// obj2 et obj1 pointent sur le même objet
obj2.prop1 = "xyzt";
console.log("obj1=", obj1);
console.log("obj2=", obj2);
// les propriété peuvent être des variables
const var1: string = 'prop1';
console.log('prop1=', obj1[var1]);
|
- lignes 5-9 : l'[interface] Obj1 annonce à l'avance les propriétés qui seront ajoutées (prop1, prop2, prop3, toutes optionnelles avec ?). La ligne [clé: string]: unknown est une signature d'index : elle autorise, en plus, l'ajout de n'importe quelle autre propriété non prévue à l'avance ;
- ligne 19 : obj1.prop3 peut aussi s'écrire obj1['prop3'] — cette dernière notation est indispensable lorsque le nom de la propriété est en fait le contenu d'une variable (lignes 28-29) ;
- lignes 22-25 : const obj2: Obj1 = obj1 est une copie de référence, pas une copie de l'objet — modifier obj2 modifie donc aussi ce que l'on voit à travers obj1, comme pour les tableaux.
Résultat de l'exécution :
| obj1= { prop1: 'abcd' }
obj1= { prop1: 'abcd', prop2: [ 1, 2, 3 ] }
obj1= { prop1: 'abcd', prop2: [ 1, 2, 3 ], prop3: true }
obj1= { prop1: 'xyzt', prop2: [ 1, 2, 3 ], prop3: true }
obj2= { prop1: 'xyzt', prop2: [ 1, 2, 3 ], prop3: true }
prop1= xyzt
|
5.2. script [obj-02]
Ce script montre un objet à plusieurs niveaux, et introduit l'objet global [JSON], qui convertit un objet en chaîne de caractères et réciproquement.
| 'use strict';
// un objet à plusieurs niveaux
interface PersonneFamille {
prénom: string;
âge: number;
père: { prénom: string; âge: number };
mère: { prénom: string; âge: number };
}
const personne: PersonneFamille = {
prénom: "martin",
âge: 12,
père: {
prénom: "paul",
âge: 45
},
mère: {
prénom: "micheline",
âge: 42
}
}
// accès aux propriétés
console.log("prénom personne=", personne.prénom);
console.log("prénom mère=", personne.mère.prénom);
personne.mère.âge = 40;
console.log("âge mère=", personne.mère.âge);
// console.log sait afficher des objets
console.log("personne=", personne);
console.log("mère=", personne.mère);
// on peut aussi afficher la chaîne jSON de l'objet
let json: string = JSON.stringify(personne);
console.log("jSON=", json);
// on peut relire le jSON
let personne2: PersonneFamille = JSON.parse(json);
console.log("père=", personne2.père);
|
- lignes 3-8 : une [interface] peut elle-même contenir des types objets imbriqués (père, mère) ;
- ligne 30 : [JSON.stringify] transforme un objet TypeScript en chaîne JSON ;
- ligne 33 : [JSON.parse] fait l'inverse — on notera que le résultat est typé PersonneFamille, ce qui suppose que la chaîne JSON respecte bien cette forme (TypeScript ne le vérifie pas à l'exécution, seulement à la compilation).
Résultat de l'exécution :
| prénom personne= martin
prénom mère= micheline
âge mère= 40
personne= {
'prénom': 'martin',
'âge': 12,
'père': { 'prénom': 'paul', 'âge': 45 },
'mère': { 'prénom': 'micheline', 'âge': 40 }
}
mère= { 'prénom': 'micheline', 'âge': 40 }
jSON= {"prénom":"martin","âge":12,"père":{"prénom":"paul","âge":45},"mère":{"prénom":"micheline","âge":40}}
père= { 'prénom': 'paul', 'âge': 45 }
|
5.3. script [obj-03]
Ce script introduit la notion de getter/setter d'une propriété d'un objet :
| 'use strict';
// getters et setters d'un objet
interface PersonneAvecNom {
_nom?: string;
nom: string;
}
const personne: PersonneAvecNom = {
// getter
get nom() {
console.log("getter nom");
return this._nom as string;
},
// setter
set nom(unNom: string) {
console.log("setter nom");
this._nom = unNom;
}
};
// setter
personne.nom = "Hercule";
// getter
console.log(personne.nom);
// l'objet lui-même
console.log("personne=", personne);
// ça n'empêche pas d'accéder à la propriété [_nom] directement
personne._nom = "xyz";
console.log("personne=", personne);
|
- lignes 3-6 : l'[interface] PersonneAvecNom déclare _nom en propriété optionnelle (préfixée d'un underscore par convention, pour signaler que c'est une propriété « privée », même si TypeScript ne l'empêche pas d'y accéder — voir le vrai encapsulement avec #champ au chapitre « Les classes ») ;
- lignes 8-11 : le [getter] — mot clé get plutôt que function — rend la valeur de _nom ;
- lignes 13-16 : le [setter] — mot clé set — reçoit la valeur affectée et peut la valider avant de la stocker ;
- ligne 20 : personne.nom = "Hercule" appelle implicitement le setter ;
- ligne 22 : personne.nom appelle implicitement le getter ;
- ligne 26 : rien n'empêche d'accéder directement à _nom — l'encapsulation par convention dépend de la discipline du développeur.
Résultat de l'exécution :
| setter nom
getter nom
Hercule
personne= { nom: [Getter/Setter], _nom: 'Hercule' }
personne= { nom: [Getter/Setter], _nom: 'xyz' }
|
On notera que [console.log] affiche [Getter/Setter] pour signaler que nom est géré par des accesseurs plutôt que d'être une valeur directe.
5.4. script [obj-04]
Trois façons d'écrire le nom d'une propriété, deux façons d'y accéder, et une notation raccourcie :
| 'use strict';
// les noms des propriétés d'un objet peuvent être littéraux [nom], être entourés d'apostrophes ['nom']
// ou de guillements ["nom"]
interface NomPrénom {
nom: string;
prénom: string;
}
// littéraux
const obj1: NomPrénom = {
nom: "martin",
prénom: "jean"
};
console.log("prénom=", obj1.prénom);
// entourés d'apostrophes
const obj2: NomPrénom = {
'nom': "martin",
'prénom': "jean"
};
console.log("nom=", obj2.nom);
// entourés de guillemets
const obj3: NomPrénom = {
"nom": "martin",
"prénom": "jean"
};
// deux syntaxes possibles pour accéder à la propriété [nom]
console.log("nom=", obj3.nom);
console.log("nom=", obj3['nom']);
// notation raccourcie équivalente à {obj1:obj1, obj2:obj2}
const obj4 = {
obj1, obj2
}
console.log("obj4=", obj4)
|
- les noms de propriétés peuvent s'écrire littéralement (nom:), entre apostrophes ('nom':) ou entre guillemets ("nom":) — les trois sont équivalents ;
- ligne 31 : obj3.nom et obj3['nom'] accèdent à la même propriété ;
- lignes 34-36 : const obj4 = { obj1, obj2 } est la notation raccourcie de { obj1: obj1, obj2: obj2 } — très utilisée dès qu'on regroupe des variables existantes dans un objet.
Résultat de l'exécution :
| prénom= jean
nom= martin
nom= martin
nom= martin
obj4= {
obj1: { nom: 'martin', 'prénom': 'jean' },
obj2: { nom: 'martin', 'prénom': 'jean' }
}
|
5.5. script [obj-05]
Un objet littéral peut avoir des propriétés de type fonction — on se rapproche alors de la notion de classe (propriétés + méthodes) :
| 'use strict';
// un objet peut avoir des propriétés de type [function]
interface PersonneAvecToString {
prénom: string;
âge: number;
père: { prénom: string; âge: number };
mère: { prénom: string; âge: number };
toString: () => string;
}
const personne: PersonneAvecToString = {
// propriétés
prénom: "martin",
âge: 12,
père: {
prénom: "paul",
âge: 45
},
mère: {
prénom: "micheline",
âge: 42
},
// méthode
toString: function () {
return JSON.stringify(this);
}
}
// usage
console.log("personne=", personne);
console.log("personne.toString=", personne.toString());
|
- ligne 4 : l'[interface] déclare toString: () => string — le type d'une méthode se déclare comme celui d'une propriété fonction ;
- ligne 25 : à l'intérieur de la méthode, [this] désigne l'objet lui-même — this.prénom est la propriété prénom de cet objet.
Résultat de l'exécution :
| personne= {
'prénom': 'martin',
'âge': 12,
'père': { 'prénom': 'paul', 'âge': 45 },
'mère': { 'prénom': 'micheline', 'âge': 42 },
toString: [Function: toString]
}
personne.toString= {"prénom":"martin","âge":12,"père":{"prénom":"paul","âge":45},"mère":{"prénom":"micheline","âge":42}}
|
5.6. script [obj-06]
Ce script montre comment parcourir les propriétés d'un objet sans en connaître les noms à l'avance :
| 'use strict';
// un objet peut avoir des propriétés de type [function]
interface PersonneAvecToString {
prénom: string;
âge: number;
père: { prénom: string; âge: number };
mère: { prénom: string; âge: number };
toString: () => string;
// index signature nécessaire pour permettre le parcours dynamique for..in ci-dessous
[clé: string]: unknown;
}
let personne: PersonneAvecToString = {
// propriétés
prénom: "martin",
âge: 12,
père: {
prénom: "paul",
âge: 45
},
mère: {
prénom: "micheline",
âge: 42
},
// méthode
toString: function () {
return JSON.stringify(this);
}
}
// usage
console.log(personne);
// propriétés
console.log("-----------------------");
for (const key in personne) {
// eslint-disable-next-line no-prototype-builtins
if (personne.hasOwnProperty(key)) {
const element = personne[key];
console.log(key, "=", element);
}
}
// pour échapper à l'avertissement eslint (1)
console.log("-----------------------");
for (const key in personne) {
if (Object.prototype.hasOwnProperty.call(personne, key)) {
const element = personne[key];
console.log(key, "=", element);
}
}
// pour échapper à l'avertissement eslint (2)
console.log("-----------------------");
for (const key in personne) {
// eslint-disable-next-line no-prototype-builtins
if (personne.hasOwnProperty(key)) {
const element = personne[key];
console.log(key, "=", element);
}
}
|
- ligne 11 : la signature d'index [clé: string]: unknown est nécessaire ici pour que TypeScript autorise l'accès dynamique personne[key] de la boucle for...in ;
- lignes 35-41 : for (const key in personne) parcourt les noms de propriétés. Le test hasOwnProperty élimine les propriétés éventuellement héritées (non présentes dans ce script, mais bonne pratique systématique) ;
- ce script propose 3 écritures équivalentes de ce test — la 2ᵉ (Object.prototype.hasOwnProperty.call(...)) est la plus robuste, la 3ᵉ se contente de désactiver l'avertissement ESLint correspondant. Depuis 2022, la méthode Object.hasOwn() (chapitre suivant, obj-09) rend ce choix inutile.
Résultat de l'exécution :
| {
'prénom': 'martin',
'âge': 12,
'père': { 'prénom': 'paul', 'âge': 45 },
'mère': { 'prénom': 'micheline', 'âge': 42 },
toString: [Function: toString]
}
-----------------------
prénom = martin
âge = 12
père = { 'prénom': 'paul', 'âge': 45 }
mère = { 'prénom': 'micheline', 'âge': 42 }
toString = [Function: toString]
-----------------------
prénom = martin
âge = 12
père = { 'prénom': 'paul', 'âge': 45 }
mère = { 'prénom': 'micheline', 'âge': 42 }
toString = [Function: toString]
-----------------------
prénom = martin
âge = 12
père = { 'prénom': 'paul', 'âge': 45 }
mère = { 'prénom': 'micheline', 'âge': 42 }
toString = [Function: toString]
|
5.7. script [obj-07]
Ce script montre la déstructuration d'un objet — une notation qui extrait directement des propriétés dans des variables :
| 'use strict';
// déstructuration
interface NomPrénom {
nom: string;
prénom: string;
}
// littéraux
const obj1: NomPrénom = {
nom: "martin",
prénom: "jean"
};
// déstructuration obj1 dans variables [n,p]
const { nom: n, prénom: p } = obj1;
console.log("n=", n, "p=", p);
// déstructuration obj1 dans variables [n2,p2]
function f({ nom: n2, prénom: p2 }: NomPrénom): void {
console.log("f-n2=", n2, "f-p2=", p2);
}
f(obj1);
// déstructuration obj1 dans variables [nom,prénom]
function g({ nom: nom, prénom: prénom }: NomPrénom): void {
console.log("g-nom=", nom, "g-prénom=", prénom);
}
g(obj1);
// déstructuration obj1 dans variables [nom,prénom]
// avec notation raccoucie équivalente à h({nom:nom,prénom:prénom})
function h({ nom, prénom }: NomPrénom): void {
console.log("h-nom=", nom, "h-prénom=", prénom);
}
h(obj1);
|
- ligne 16 : const { nom: n, prénom: p } = obj1 crée deux variables n et p, équivalent à const n = obj1.nom; const p = obj1.prénom ;
- lignes 19-22, 24-27, 30-33 : la déstructuration fonctionne aussi directement dans les paramètres d'une fonction — très courant en TypeScript/React pour extraire des propriétés d'un objet de configuration ou de props ;
- ligne 33 : function h({ nom, prénom }: NomPrénom) est un raccourci de { nom: nom, prénom: prénom } — quand le nom de la variable créée est identique au nom de la propriété, on peut l'omettre.
Résultat de l'exécution :
| n= martin p= jean
f-n2= martin f-p2= jean
g-nom= martin g-prénom= jean
h-nom= martin h-prénom= jean
|
5.8. script [obj-08]
Ce script montre comment obtenir une copie superficielle d'un objet, grâce à l'opérateur de spread ... :
| 'use strict'
// clônage d'objets
interface NomPrénom {
nom: string;
prénom: string;
}
const obj1: NomPrénom = {
nom: "martin",
prénom: "jean"
};
// clône de obj1 avec l'opérateur de spread
const obj2: NomPrénom = { ...obj1 }
// vérifications
// obj2 pointe sur une copie de obj1
console.log("obj2===obj1 :", obj1 === obj2)
console.log("obj2=", obj2)
|
Résultat de l'exécution :
| obj2===obj1 : false
obj2= { nom: 'martin', 'prénom': 'jean' }
|
- obj2===obj1 vaut false : les deux références ne pointent pas sur le même objet ;
- obj2 contient bien une copie des propriétés de obj1 — mais attention, comme on le verra au script suivant, c'est une copie superficielle seulement.
5.9. script [obj-09]
[NOUVEAU depuis 2019] Ce script présente trois compléments pratiques apparus depuis 2020 : [Object.hasOwn], [structuredClone], et la combinaison du chaînage optionnel avec la coalescence des nuls sur des objets imbriqués.
| 'use strict';
// ========================================================================
// [NOUVEAU depuis 2019] manipulation d'objets : quelques ajouts pratiques
// ========================================================================
// ------------------------------------------------------------------------
// 1) Object.hasOwn(objet, propriété) (ECMAScript 2022)
// ------------------------------------------------------------------------
// avant, pour savoir si une propriété appartient en direct à un objet (et n'est pas
// héritée par la chaîne des prototypes), on écrivait :
// objet.hasOwnProperty(propriété) -> peut planter si l'objet redéfinit hasOwnProperty
// Object.prototype.hasOwnProperty.call(objet, propriété) -> correct mais verbeux (cf. objets/obj-06.js)
// [Object.hasOwn] fait ce travail simplement et sans risque
interface NomPrénom {
nom: string;
prénom: string;
[clé: string]: unknown;
}
const personne: NomPrénom = { nom: "Dupont", prénom: "Jean" };
console.log("Object.hasOwn(personne, 'nom') =", Object.hasOwn(personne, "nom"));
console.log("Object.hasOwn(personne, 'âge') =", Object.hasOwn(personne, "âge"));
// utilisation typique dans un parcours de propriétés
console.log("-----------------------");
for (const clé in personne) {
if (Object.hasOwn(personne, clé)) {
console.log(clé, "=", personne[clé]);
}
}
// ------------------------------------------------------------------------
// 2) structuredClone(objet) (API globale disponible depuis node 17)
// ------------------------------------------------------------------------
// obj-08.js montre le clônage superficiel (shallow) avec l'opérateur de spread [...obj]
// mais un spread ne clône que le 1er niveau : les sous-objets restent partagés !
interface PersonneAvecAdresse {
nom: string;
adresse: { ville: string; codePostal: string };
}
const original: PersonneAvecAdresse = {
nom: "Dupont",
adresse: { ville: "Nantes", codePostal: "44000" }
};
// clônage superficiel avec spread
const copieSuperficielle: PersonneAvecAdresse = { ...original };
copieSuperficielle.adresse.ville = "Angers"; // modifie aussi l'adresse de [original] !
console.log("copie superficielle a modifié l'original :", original.adresse.ville === "Angers");
// remise à zéro pour le test suivant
original.adresse.ville = "Nantes";
// [structuredClone] fait un clônage profond (deep clone) : tous les sous-objets sont recopiés
const copieProfonde: PersonneAvecAdresse = structuredClone(original);
copieProfonde.adresse.ville = "Angers";
console.log("copie profonde n'a pas modifié l'original :", original.adresse.ville === "Nantes");
console.log("original =", original);
console.log("copieProfonde =", copieProfonde);
// ------------------------------------------------------------------------
// 3) chaînage optionnel + nullish sur des méthodes d'objet imbriquées
// ------------------------------------------------------------------------
// combinaison très fréquente en pratique pour lire une configuration partielle
interface ConfigurationApparence {
apparence?: { thème?: string };
}
function afficheThème(configuration?: ConfigurationApparence): void {
// si configuration, configuration.apparence ou configuration.apparence.thème est absent,
// on retombe sur "clair" sans jamais planter
const thème = configuration?.apparence?.thème ?? "clair";
console.log("thème =", thème);
}
afficheThème({ apparence: { thème: "sombre" } });
afficheThème({ apparence: {} });
afficheThème({});
afficheThème(undefined);
|
- [Object.hasOwn(objet, propriété)] (ES2022) remplace avantageusement objet.hasOwnProperty(...) (voir script obj-06) : plus simple à lire, et jamais piégé si l'objet redéfinit lui-même hasOwnProperty ;
- [structuredClone(objet)] (disponible nativement depuis Node 17) fait un clônage profond : contrairement au spread { ...obj } du script obj-08, qui ne recopie que le premier niveau, structuredClone recopie aussi les sous-objets — modifier la copie ne modifie jamais l'original, à quelque niveau que ce soit ;
- la fonction afficheThème combine ?. et ?? pour lire une configuration à plusieurs niveaux, potentiellement incomplète, sans jamais planter et avec une valeur de repli claire.
Résultat de l'exécution :
| Object.hasOwn(personne, 'nom') = true
Object.hasOwn(personne, 'âge') = false
-----------------------
nom = Dupont
prénom = Jean
copie superficielle a modifié l'original : true
copie profonde n'a pas modifié l'original : true
original = { nom: 'Dupont', adresse: { ville: 'Nantes', codePostal: '44000' } }
copieProfonde = { nom: 'Dupont', adresse: { ville: 'Angers', codePostal: '44000' } }
thème = sombre
thème = clair
thème = clair
thème = clair
|
5.10. Conclusion
Les scripts de ce chapitre ont montré que l'objet littéral TypeScript est proche de l'objet instance de classe : on peut y définir propriétés, méthodes, getters/setters — avec, en plus, une [interface] qui en décrit précisément la forme à la compilation. C'est un objet dynamique, comparable à un dictionnaire, dont les éléments peuvent être de tout type, y compris des fonctions.