[object Promise] en TypeScript peut sembler mystérieux au premier abord, mais il devient encore plus déroutant quand il génère des erreurs incompréhensibles. Si vous avez déjà vu des messages comme "Type 'string' is not assignable to type 'never'" et que vous vous êtes demandé ce qui se passait, cet article est fait pour vous.

Nous allons explorer ensemble les erreurs les plus courantes liées au type never, comprendre leur origine et surtout apprendre à les résoudre efficacement.

Pourquoi le type Never génère-t-il autant d'erreurs ?

Le type never représente une valeur qui ne devrait jamais exister. Il est souvent le résultat d'un système de types qui a déterminé qu'une branche de code est inaccessible. Quand TypeScript infère never alors que vous attendez autre chose, c'est généralement le signe d'une incohérence dans votre typage.

Les 5 erreurs Never les plus courantes

1. "Type 'string' is not assignable to type 'never'"

C'est probablement l'erreur la plus frustrante. Elle survient souvent dans ce type de situation :

function processValue(value: string | number) {
  if (typeof value === 'string') {
    // Traitement des strings
  } else if (typeof value === 'number') {
    // Traitement des numbers
  } else {
    // TypeScript infère que value est de type 'never' ici
    console.log(value); // ❌ Erreur !
  }
}

Solution : Le problème vient du fait que TypeScript a correctement déterminé que la branche else est inaccessible. Si vous voulez vraiment gérer ce cas, ajoutez une assertion de type ou revoyez votre logique :

2. Arrays qui deviennent never[]

Quand vous déclarez un array vide sans spécifier son type, TypeScript peut l'inférer comme never[] :

const items = []; // Type inféré: never[]
items.push('hello'); // ❌ Erreur: Argument of type 'string' is not assignable to parameter of type 'never'

Solutions :

Il existe plusieurs solutions pour cela avec un typage explicite, une assertion, ou une initialisation de l'array

// Option 1: Typage explicite
const items: string[] = [];

// Option 2: Assertion de type
const items = [] as string[];

// Option 3: Initialisation avec une valeur
const items = ['premier-item'];

3. Erreurs dans les unions exhaustives

Cette erreur survient souvent avec les switch statements sur des unions de types :

type Status = 'pending' | 'approved' | 'rejected';

function handleStatus(status: Status) {
  switch (status) {
    case 'pending':
      return 'En attente';
    case 'approved':
      return 'Approuvé';
    // Oubli du cas 'rejected'
    default:
      // status est de type 'rejected' ici, pas 'never'
      throw new Error(`Status non géré: ${status}`); // ❌ Peut causer des erreurs
  }
}

Solution avec exhaustive checking :

function handleStatus(status: Status): string {
  switch (status) {
    case 'pending':
      return 'En attente';
    case 'approved':
      return 'Approuvé';
    case 'rejected':
      return 'Rejeté';
    default:
      // Ici status est vraiment de type 'never'
      const exhaustiveCheck: never = status;
      throw new Error(`Status non géré: ${exhaustiveCheck}`);
  }
}

4. "Function lacks ending return statement"

Cette erreur apparaît quand TypeScript détecte qu'une fonction pourrait ne pas retourner de valeur :

function getStatusMessage(status: 'active' | 'inactive'): string {
  if (status === 'active') {
    return 'Actif';
  }
  // ❌ TypeScript ne garantit pas qu'on retourne toujours une valeur
}

Solutions

// Option 1: Gérer tous les cas
function getStatusMessage(status: 'active' | 'inactive'): string {
  if (status === 'active') {
    return 'Actif';
  } else {
    return 'Inactif';
  }
}

// Option 2: Utiliser un switch avec default
function getStatusMessage(status: 'active' | 'inactive'): string {
  switch (status) {
    case 'active':
      return 'Actif';
    case 'inactive':
      return 'Inactif';
    default:
      const exhaustiveCheck: never = status;
      throw new Error(`Status non géré: ${exhaustiveCheck}`);
  }
}

Diagnostic pas à pas : Comment débugger une erreur Never

1. Analyser le message d'erreur

TypeScript donne généralement des indices précieux dans ses messages d'erreur. Cherchez :

  • Le type attendu vs le type fourni
  • La ligne exacte où l'erreur survient
  • Le contexte de l'inférence de type

2. Utiliser les outils de développement

TypeScript Playground : Collez votre code sur typescriptlang.org/play pour voir les types inférés en temps réel.

IDE avec hover : Survolez vos variables pour voir leur type inféré :

function example(value: string | number) {
  if (typeof value === 'string') {
    // Hover sur 'value' → type: string
  } else {
    // Hover sur 'value' → type: number
  }
}

3. Technique de narrowing progressif

Ajoutez des assertions de type ou des logs temporaires pour comprendre où l'inférence se casse :

function debug(value: unknown) {
  console.log('Type initial:', typeof value);
  
  if (typeof value === 'string') {
    console.log('Dans la branche string');
    // Votre code ici
  } else if (typeof value === 'number') {
    console.log('Dans la branche number');
    // Votre code ici
  } else {
    console.log('Dans else, type:', typeof value);
    // Si vous arrivez ici avec une valeur valide, 
    // c'est qu'il y a un problème dans votre logique
  }
}

Solutions concrètes et bonnes pratiques

Refactoring des unions complexes

Pour les unions complexes, préférez les discriminated unions :

// ❌ Union simple, difficile à gérer
type ApiResponse = {
  data?: any;
  error?: string;
  status: number;
}

// ✅ Discriminated union
type ApiResponse = 
  | { success: true; data: any; status: number }
  | { success: false; error: string; status: number };

function handleResponse(response: ApiResponse) {
  if (response.success) {
    // TypeScript sait que data existe
    console.log(response.data);
  } else {
    // TypeScript sait que error existe
    console.log(response.error);
  }
}

Utiliser des assertions de type avec parcimonie

Les assertions de type peuvent masquer les vrais problèmes :

// ❌ Masque le problème
const value = getData() as string;

// ✅ Mieux : vérification explicite
const rawValue = getData();
if (typeof rawValue === 'string') {
  const value = rawValue; // Type narrowé naturellement
}

Configuration TypeScript recommandée

Dans votre tsconfig.json, activez ces options pour détecter les problèmes plus tôt

{
  "compilerOptions": {
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

Conclusion

Les erreurs liées au type never en TypeScript sont souvent le symptôme d'un problème plus profond dans la conception de vos types. En comprenant leur origine et en appliquant les bonnes pratiques présentées dans cet article, vous pourrez non seulement les résoudre mais aussi les prévenir.

Le type never n'est pas votre ennemi : c'est un outil puissant pour créer du code plus robuste. Une fois maîtrisé, il devient un allié précieux pour détecter les incohérences logiques dans votre code.

Points clés à retenir :

  • Les erreurs never signalent souvent des incohérences logiques
  • Utilisez les outils de debug (hover, TypeScript Playground)
  • Préférez les discriminated unions pour les types complexes
  • L'exhaustive checking vous protège des oublis
  • Une configuration TypeScript stricte détecte les problèmes plus tôt

En maîtrisant ces concepts, vous transformerez les erreurs TypeScript frustrantes en opportunités d'améliorer la qualité de votre code.