Nous sommes parfois amenés à traiter des objets similaires en TypeScript et il faut de temps en temps les différencier pour adapter leur traitement.
Imaginons que nous ayons des entités Employee qui héritent une partie de leurs propriétés d’une entité de base Person.
Voyons comment, à l’aide d’exemples simples, nous pouvons différencier leur traitement en faisant du type checking.
(NB : les noms des entités précisent volontairement les types d’entités par soucis de clarté)
Avec des classes
Nous sommes en effet ici dans un cas typique où la POO peut nous apporter pas mal d’avantages.
class PersonClass {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
}
class EmployeeClass extends PersonClass {
occupation: string;
department: string;
constructor(
firstName: string,
lastName: string,
occupation: string,
department: string
) {
super(firstName, lastName);
this.occupation = occupation;
this.department = department;
}
}
Supposons que nous ayons besoin d’une fonction qui nous fournisse une chaîne de caractères représentant les informations des Person/Employee traités. Nous pourrions utiliser le mot clé instanceof pour déterminer de quelle instance de classe est l’objet traité.
class PersonClass {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
}
class EmployeeClass extends PersonClass {
occupation: string;
department: string;
constructor(
firstName: string,
lastName: string,
occupation: string,
department: string
) {
super(firstName, lastName);
this.occupation = occupation;
this.department = department;
}
}
function toString(person: PersonClass | EmployeeClass): string {
if (person instanceof EmployeeClass) {
return `${person.firstName} ${person.lastName} - ${person.occupation} | ${person.department}`;
}
return `${person.firstName} ${person.lastName}`;
}
Ok, Ok ! Je vois d’ici les puristes s’exclamer, et à raison.
Nous sommes ici dans l’illustration.
Tant qu’à faire de la POO, nous devrions plutôt déclarer notre fonction toString() dans la classe !
class PersonClass {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
public toString(): string {
return `${this.firstName} ${this.lastName}`;
}
}
class EmployeeClass extends PersonClass {
occupation: string;
department: string;
constructor(
firstName: string,
lastName: string,
occupation: string,
department: string
){
super(firstName, lastName);
this.occupation = occupation;
this.department = department;
}
public toString(): string {
return `${this.firstName} ${this.lastName} - ${this.occupation} | ${this.department}`;
}
}
function toString(person: PersonClass | EmployeeClass): string {
return person.toString();
}
L’inconvénient des classes dans une utilisation classique dans un frontend est qu’il faudra à chaque réponse d’une requête HTTP ou à chaque création d’un objet, créer les instances de classe avec le constructeur. (new EmployeeClass(firstName, ...))
Avec des types / interfaces
Reprenons l’exemple précédent mais en utilisant des objets et des types. Nous pourrions écrire :
type PersonType = { firstName: string; lastName: string };
type EmployeeType = PersonType & { occupation: string; department: string };
function toString(person: PersonType | EmployeeType): string {
return `${person.firstName} ${person.lastName} - ${person.occupation} | ${person.department}`;
}
Se pose alors le problème suivant : comment discerner les objets selon leur type pour éviter l’erreur ci-dessous ?

Nous pourrions commencer par écrire la fonction toString() comme ceci.
function toString(person: PersonType | EmployeeType): string {
if ("occupation" in person) {
return `${person.firstName} ${person.lastName} - ${person.occupation} | ${person.department}`;
}
return `${person.firstName} ${person.lastName}`;
}
Attention, le mot clé in sert à déterminer si la propriété existe dans l’objet, pas sa valeur.
const obj = {
foo: "bar",
baz: "qux",
};
console.log("foo" in obj); // true
console.log("bar" in obj); // false
const arr = [1, 2, 3];
console.log(1 in arr); // true
console.log(3 in arr); // false
Le test sur la présence de la propriété est suffisant dans ce cas, mais pas totalement discriminant.
Certains pourraient être tentés d’utiliser l’opérateur typeof pour savoir de quel type est l’objet. Mais cela ne fonctionne qu’avec les types primitifs.
function testType(p: Person | Employee) {
return typeof p;
}
const employee: Employee = {
firstName: "Jane",
lastName: "Doe",
occupation: "Developer",
department: "Engineering",
};
console.log(testType(employee)); // 'object'
Nous pouvons, par contre, créer une fonction qui va nous permettre de déterminer si l’objet est exactement du type PersonType et une autre pour savoir si l’objet est exactement du type EmployeeType. Vous noterez l’utilisation du type unknown qui signifie que le paramètre aura un type déterminé qu’on ne connaît pas pour le moment, à l’inverse de any qui veut plutôt dire que le paramètre sera de n’importe quel type.
function isPersonType(entity: unknown): boolean {
return (
typeof entity === "object" &&
entity != null &&
"firstName" in entity &&
"lastName" in entity &&
Object.keys(entity).length === 2
);
}
function isEmployeeType(entity: unknown): boolean {
return (
isPersonType(entity) &&
"occupation" in (entity as object) &&
"department" in (entity as object) &&
Object.keys(entity as object).length === 4
);
}
function toString(entity: PersonType | EmployeeType): string {
if (isEmployeeType(entity)) {
return `${entity.firstName} ${entity.lastName} - ${entity.occupation} | ${entity.department}`;
} else if (isPersonType(entity)) {
return `${entity.firstName} ${entity.lastName}`;
}
throw new Error("Entity is not a PersonType or EmployeeType.");
}
Même si ce code est tout à fait valable, TypeScript nous donne 3 erreurs :


En effet, nous avons réussi à déterminer si toutes les caractéristiques de nos entités passées en paramètres de nos fonctions répondaient à celles attendues par nos types, mais nous n’avons pas indiqué à TypeScript que nos entités sont bien de ces types.
Pour cela, nous avons l’opérateur is qui va se positionner sur le type de retour des fonctions précédentes.
function isPersonType(entity: unknown): entity is PersonType {
return (
typeof entity === "object" &&
entity != null &&
"firstName" in entity &&
"lastName" in entity &&
Object.keys(entity).length === 2
);
}
function isEmployeeType(entity: unknown): entity is EmployeeType {
return (
isPersonType(entity) &&
"occupation" in entity &&
"department" in entity &&
Object.keys(entity).length === 4
);
}
L’opérateur is est utile ici uniquement pour une vérification statique (static type checking = avant de transpiler le code, dans l’IDE). En effet, le code JavaScript généré n’en tient pas compte :
function isPerson(entity) {
return (
typeof entity === "object" &&
entity != null &&
"firstName" in entity &&
"lastName" in entity &&
Object.keys(entity).length === 2
);
}
function isEmployee(entity) {
return (
isPerson(entity) &&
"occupation" in entity &&
"department" in entity &&
Object.keys(entity).length === 4
);
}
function toString(person) {
if (isEmployee(person)) {
return `${person.firstName} ${person.lastName} - ${person.occupation} | ${person.department}`;
} else if (isPerson(person)) {
return `${person.firstName} ${person.lastName}`;
}
throw new Error("person is not of type Person or Employee");
}
Par honnêteté intellectuelle, je me dois de vous dire que ces fonctions ne sont pas sûres à 100% non plus. On pourrait augmenter leur niveau de sûreté en vérifiant le type de chacun des attributs! 😅
Chaque développeur doit estimer où placer le curseur de la sûreté en fonction du besoin.