~3 min de lecture
?? vs || : L'Opérateur Que 90% Des Devs Utilisent Mal
Tu utilises || partout dans ton code TypeScript ? Tu penses que c'est la même chose que ?? ?
Spoiler alert : Non. Et cette confusion peut créer des bugs subtils et difficiles à débugger.
Dans cet article, je vais te montrer la différence cruciale entre ces deux opérateurs et pourquoi ?? devrait
devenir ton nouveau réflexe.
Le Problème Avec L'Opérateur OR (||)
❌ Ce que font les juniors :
Ils utilisent || pour définir des valeurs par défaut, sans se rendre compte des pièges.
interface UserConfig {
readonly name: string;
readonly age: number;
readonly isAdmin: boolean;
readonly score: number;
}
function displayUserInfo(config: UserConfig): void {
// ❌ Problème : || considère 0, false, '' comme falsy
const name = config.name || 'Anonyme';
const age = config.age || 18;
const isAdmin = config.isAdmin || false;
const score = config.score || 0;
console.log({ name, age, isAdmin, score });
}
// Test
displayUserInfo({
name: '',
age: 0,
isAdmin: false,
score: 0
});
// ❌ Résultat inattendu :
// { name: 'Anonyme', age: 18, isAdmin: false, score: 0 }
// Alors qu'on voulait garder les valeurs 0, false et ''
🚨 Pourquoi c'est problématique ?
L'opérateur || retourne la première valeur truthy (vraie). Il considère comme falsy :
false0''(chaîne vide)nullundefinedNaN
Résultat : tu remplaces des valeurs légitimes par des valeurs par défaut !
La Solution Pro : L'Opérateur Nullish Coalescing (??)
✅ Ce que font les pros :
Ils utilisent ?? qui ne considère que null et undefined comme "absents".
interface UserConfig {
readonly name: string;
readonly age: number;
readonly isAdmin: boolean;
readonly score: number;
}
function displayUserInfo(config: UserConfig): void {
// ✅ ?? ne remplace que null et undefined
const name = config.name ?? 'Anonyme';
const age = config.age ?? 18;
const isAdmin = config.isAdmin ?? false;
const score = config.score ?? 0;
console.log({ name, age, isAdmin, score });
}
// Test
displayUserInfo({
name: '',
age: 0,
isAdmin: false,
score: 0
});
// ✅ Résultat correct :
// { name: '', age: 0, isAdmin: false, score: 0 }
// Les valeurs 0, false et '' sont conservées !
💡 Pourquoi c'est mieux ?
- Plus prévisible : seuls
nulletundefinedsont remplacés - Moins de bugs : tu gardes les valeurs légitimes comme
0,false,'' - Plus explicite : ton intention est claire (gérer l'absence de valeur, pas les valeurs falsy)
Comparaison Détaillée : || vs ??
Tableau de Comparaison
| Valeur | value || 'default' |
value ?? 'default' |
|---|---|---|
null |
'default' |
'default' |
undefined |
'default' |
'default' |
false |
'default' ❌ |
false ✅ |
0 |
'default' ❌ |
0 ✅ |
'' |
'default' ❌ |
'' ✅ |
NaN |
'default' ❌ |
NaN ✅ |
'hello' |
'hello' |
'hello' |
Exemples Concrets
// Exemple 1 : Score de jeu
const score = 0;
console.log(score || 100); // ❌ 100 (mauvais !)
console.log(score ?? 100); // ✅ 0 (correct !)
// Exemple 2 : Chaîne vide
const username = '';
console.log(username || 'Guest'); // ❌ 'Guest' (mauvais !)
console.log(username ?? 'Guest'); // ✅ '' (correct !)
// Exemple 3 : Boolean false
const isEnabled = false;
console.log(isEnabled || true); // ❌ true (mauvais !)
console.log(isEnabled ?? true); // ✅ false (correct !)
// Exemple 4 : null/undefined (même comportement)
const data = null;
console.log(data || 'default'); // ✅ 'default'
console.log(data ?? 'default'); // ✅ 'default'
Cas d'Usage Réels En Angular
Exemple 1 : Configuration de composant avec des valeurs par défaut
@Component({
selector: 'app-button',
standalone: true,
template: `
<button
[disabled]="disabled()"
[class]="'btn btn-' + variant()">
{{ label() }}
</button>
`
})
export class ButtonComponent {
// ❌ Mauvaise approche avec ||
readonly label = input<string>();
readonly variant = input<string>();
readonly disabled = input<boolean>();
readonly displayLabel = computed(() => this.label() || 'Click me');
readonly displayVariant = computed(() => this.variant() || 'primary');
readonly isDisabled = computed(() => this.disabled() || false);
// Problème : si disabled=false est passé, il devient true !
}
// ✅ Bonne approche avec ??
@Component({
selector: 'app-button',
standalone: true,
template: `
<button
[disabled]="isDisabled()"
[class]="'btn btn-' + displayVariant()">
{{ displayLabel() }}
</button>
`
})
export class ButtonComponent {
readonly label = input<string>();
readonly variant = input<string>();
readonly disabled = input<boolean>();
readonly displayLabel = computed(() => this.label() ?? 'Click me');
readonly displayVariant = computed(() => this.variant() ?? 'primary');
readonly isDisabled = computed(() => this.disabled() ?? false);
}
Exemple 2 : Gestion de données API avec valeurs nullables
interface ApiUser {
readonly id: number;
readonly name: string | null;
readonly email: string | null;
readonly age: number | null;
readonly isActive: boolean | null;
}
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
<div class="profile">
<h2>{{ displayName() }}</h2>
<p>Email: {{ displayEmail() }}</p>
<p>Age: {{ displayAge() }}</p>
<p>Status: {{ displayStatus() }}</p>
</div>
`
})
export class UserProfileComponent {
private readonly userService = inject(UserService);
readonly user = toSignal(this.userService.getUser());
// ✅ Utilisation de ?? pour les valeurs par défaut
readonly displayName = computed(() =>
this.user()?.name ?? 'Nom non renseigné'
);
readonly displayEmail = computed(() =>
this.user()?.email ?? 'Email non renseigné'
);
readonly displayAge = computed(() =>
this.user()?.age ?? 'Âge non renseigné'
);
readonly displayStatus = computed(() =>
(this.user()?.isActive ?? false) ? 'Actif' : 'Inactif'
);
}
Exemple 3 : Formulaires avec valeurs par défaut
@Component({
selector: 'app-user-form',
standalone: true,
imports: [ReactiveFormsModule]
})
export class UserFormComponent implements OnInit {
private readonly fb = inject(FormBuilder);
readonly form = this.fb.group({
name: [''],
age: [0],
acceptTerms: [false],
newsletter: [false]
});
ngOnInit(): void {
// Charger les données utilisateur existantes
const savedData = this.loadSavedData();
// ✅ Utiliser ?? pour préserver les valeurs 0 et false
this.form.patchValue({
name: savedData.name ?? '',
age: savedData.age ?? 0,
acceptTerms: savedData.acceptTerms ?? false,
newsletter: savedData.newsletter ?? false
});
}
private loadSavedData(): Partial<{
readonly name: string;
readonly age: number;
readonly acceptTerms: boolean;
readonly newsletter: boolean;
}> {
// Simuler le chargement depuis localStorage ou API
return {
name: null as any,
age: 0, // ⚠️ Cette valeur doit être conservée
acceptTerms: false, // ⚠️ Cette valeur doit être conservée
newsletter: undefined as any
};
}
}
L'Opérateur Nullish Assignment (??=)
Bonus : Une syntaxe encore plus courte
TypeScript offre aussi l'opérateur ??= pour assigner une valeur seulement si la variable est null ou undefined.
let count: number | null = null;
let name: string | undefined = undefined;
let isActive = false;
let score = 0;
// ✅ Assigne seulement si null ou undefined
count ??= 10; // count = 10
name ??= 'John'; // name = 'John'
isActive ??= true; // isActive reste false (pas null/undefined)
score ??= 100; // score reste 0 (pas null/undefined)
console.log({ count, name, isActive, score });
// { count: 10, name: 'John', isActive: false, score: 0 }
Exemple pratique en Angular
@Component({
selector: 'app-settings',
standalone: true
})
export class SettingsComponent implements OnInit {
private readonly storageService = inject(StorageService);
// Configuration avec valeurs par défaut
readonly config = signal({
theme: undefined as string | undefined,
fontSize: null as number | null,
autoSave: undefined as boolean | undefined,
language: null as string | null
});
ngOnInit(): void {
const savedConfig = this.storageService.getConfig();
// ✅ Utiliser ??= pour définir les valeurs par défaut
savedConfig.theme ??= 'light';
savedConfig.fontSize ??= 14;
savedConfig.autoSave ??= true;
savedConfig.language ??= 'fr';
this.config.set(savedConfig);
}
}
Quand Utiliser || vs ?? ?
Utilise || quand :
✅ Tu veux remplacer toutes les valeurs falsy (y compris 0, false, '')
✅ Tu travailles avec des chaînes et veux éviter les chaînes vides
✅ Tu veux un comportement "tout ou rien"
// Cas légitime pour ||
const displayText = userInput || 'Texte par défaut';
const validEmail = email.trim() || null;
const searchQuery = query.trim() || undefined;
Utilise ?? quand :
✅ Tu veux remplacer uniquement null et undefined
✅ Tu travailles avec des nombres (où 0 est une valeur valide)
✅ Tu travailles avec des booleans (où false est une valeur valide)
✅ Tu veux des valeurs par défaut tout en gardant 0, false, ''
// Cas typiques pour ??
const age = user.age ?? 0;
const isEnabled = config.enabled ?? false;
const count = data.count ?? 0;
const score = game.score ?? 0;
Les Pièges À Éviter
Piège 1 : Mélanger || et ?? sans parenthèses
// ❌ Erreur de syntaxe TypeScript
const value = a || b ?? c;
// ✅ Utiliser des parenthèses
const value = (a || b) ?? c;
const value2 = a || (b ?? c);
Piège 2 : Utiliser ?? avec des types non-nullable
// ❌ TypeScript te prévient que c'est inutile
const name: string = 'John';
const result = name ?? 'Default'; // name ne peut jamais être null/undefined
// ✅ Utiliser ?? seulement avec des types nullable
const nullableName: string | null = 'John';
const finalResult = nullableName ?? 'Default';
Piège 3 : Oublier que ?? ne gère pas NaN
const value = NaN;
console.log(value || 0); // ✅ 0 (NaN est falsy)
console.log(value ?? 0); // ❌ NaN (NaN n'est ni null ni undefined)
// ✅ Pour gérer NaN, utilise isNaN()
const safeValue = isNaN(value) ? 0 : value;
Conclusion : ?? Est Ton Nouvel Ami
L'opérateur ?? n'est pas juste une alternative à ||, c'est un outil plus précis pour gérer les valeurs absentes.
Ce que tu dois retenir :
✅ ?? ne remplace que null et undefined
✅ || remplace toutes les valeurs falsy (y compris 0, false, '')
✅ Utilise ?? par défaut pour les valeurs par défaut
✅ Utilise || seulement quand tu veux vraiment filtrer les valeurs falsy
✅ ??= pour assigner des valeurs par défaut de manière concise
La règle d'or :
Si 0, false ou '' sont des valeurs valides dans ton contexte, utilise ?? plutôt que ||.
Envie de découvrir EasyAngularKit et son programme ?
→ Découvre EasyAngularKit : https://pim.ms/C31g7p2