Maison  >  Article  >  interface Web  >  Exceptions riches au moment de la compilation dans TypeScript utilisant des types non constructibles

Exceptions riches au moment de la compilation dans TypeScript utilisant des types non constructibles

Susan Sarandon
Susan Sarandonoriginal
2024-10-29 18:40:02972parcourir

Rich Compile-Time Exceptions in TypeScript Using Unconstructable Types

Le système de types de TypeScript est puissant, mais ses messages d'erreur peuvent parfois être énigmatiques et difficiles à comprendre. Dans cet article, nous explorerons un modèle qui utilise des types non constructibles pour créer des exceptions claires et descriptives au moment de la compilation. Cette approche permet d'éviter les erreurs d'exécution en rendant les états non valides non représentables avec des messages d'erreur utiles.

Le modèle : types non constructibles avec messages personnalisés

Tout d'abord, décomposons le modèle de base :

// Create a unique symbol for our type exception
declare const TypeException: unique symbol;

// Basic type definitions
type Struct = Record<string, any>;
type Funct<T, R> = (arg: T) => R;
type Types<T> = keyof T & string;
type Sanitize<T> = T extends string ? T : never;

// The core pattern for type-level exceptions
export type Unbox<T extends Struct> = {
    [Type in Types<T>]: T[Type] extends Funct<any, infer Ret>
        ? (arg: Ret) => any
        : T[Type] extends Struct
        ? {
              [TypeException]: `Variant <${Sanitize<Type>}> is of type <Union>. Migrate logic to <None> variant to capture <${Sanitize<Type>}> types.`;
          }
        : (value: T[Type]) => any;
};

Comment ça marche

  1. TypeException est un symbole unique qui agit comme une clé spéciale pour nos messages d'erreur
  2. Lorsque nous rencontrons un état de type invalide, nous renvoyons un type d'objet avec une propriété TypeException
  3. Ce type est inconstructible au moment de l'exécution, obligeant TypeScript à afficher notre message d'erreur personnalisé
  4. Le message d'erreur peut inclure des informations de type à l'aide de littéraux de modèle

Exemple 1 : Gestion des variantes avec des erreurs personnalisées

Voici un exemple montrant comment utiliser ce modèle avec des types de variantes :

type DataVariant = 
    | { type: 'text'; content: string }
    | { type: 'number'; value: number }
    | { type: 'complex'; nested: { data: string } };

type VariantHandler = Unbox<{
    text: (content: string) => void;
    number: (value: number) => void;
    complex: { // This will trigger our custom error
        [TypeException]: `Variant <complex> is of type <Union>. Migrate logic to <None> variant to capture <complex> types.`
    };
}>;

// This will show our custom error at compile time
const invalidHandler: VariantHandler = {
    text: (content) => console.log(content),
    number: (value) => console.log(value),
    complex: (nested) => console.log(nested) // Error: Type has unconstructable signature
};

Exemple 2 : validation de type récursive

Voici un exemple plus complexe montrant comment utiliser le modèle avec des types récursifs :

type TreeNode<T> = {
    value: T;
    children?: TreeNode<T>[];
};

type TreeHandler<T> = Unbox<{
    leaf: (value: T) => void;
    node: TreeNode<T> extends Struct
        ? {
              [TypeException]: `Cannot directly handle node type. Use leaf handler for individual values.`;
          }
        : never;
}>;

// Usage example - will show custom error
const invalidTreeHandler: TreeHandler<string> = {
    leaf: (value) => console.log(value),
    node: (node) => console.log(node) // Error: Cannot directly handle node type
};

Exemple 3 : validation de l'état de type

Voici comment utiliser le modèle pour appliquer des transitions d'état de type valides :

type LoadingState<T> = {
    idle: null;
    loading: null;
    error: Error;
    success: T;
};

type StateHandler<T> = Unbox<{
    idle: () => void;
    loading: () => void;
    error: (error: Error) => void;
    success: (data: T) => void;
    // Prevent direct access to state object
    state: LoadingState<T> extends Struct
        ? {
              [TypeException]: `Cannot access state directly. Use individual handlers for each state.`;
          }
        : never;
}>;

// This will trigger our custom error
const invalidStateHandler: StateHandler<string> = {
    idle: () => {},
    loading: () => {},
    error: (e) => console.error(e),
    success: (data) => console.log(data),
    state: (state) => {} // Error: Cannot access state directly
};

Quand utiliser ce modèle

Ce modèle est particulièrement utile lorsque :

  1. Vous devez empêcher certaines combinaisons de types au moment de la compilation
  2. Vous souhaitez fournir des messages d'erreur clairs et descriptifs pour les violations de type
  3. Vous construisez des hiérarchies de types complexes où certaines opérations doivent être restreintes
  4. Vous devez guider les développeurs vers des modèles d'utilisation corrects avec des messages d'erreur utiles

Détails techniques

Décomposons le fonctionnement interne du modèle :

// Create a unique symbol for our type exception
declare const TypeException: unique symbol;

// Basic type definitions
type Struct = Record<string, any>;
type Funct<T, R> = (arg: T) => R;
type Types<T> = keyof T & string;
type Sanitize<T> = T extends string ? T : never;

// The core pattern for type-level exceptions
export type Unbox<T extends Struct> = {
    [Type in Types<T>]: T[Type] extends Funct<any, infer Ret>
        ? (arg: Ret) => any
        : T[Type] extends Struct
        ? {
              [TypeException]: `Variant <${Sanitize<Type>}> is of type <Union>. Migrate logic to <None> variant to capture <${Sanitize<Type>}> types.`;
          }
        : (value: T[Type]) => any;
};

Avantages par rapport aux approches traditionnelles

  1. Effacer les messages d'erreur : au lieu des erreurs de type par défaut de TypeScript, vous recevez des messages personnalisés qui expliquent exactement ce qui n'a pas fonctionné
  2. Sécurité au moment de la compilation : toutes les erreurs sont détectées pendant le développement, pas au moment de l'exécution
  3. Auto-documentation : les messages d'erreur peuvent inclure des instructions sur la façon de résoudre le problème
  4. Type-Safe : maintient une sécurité de type totale tout en offrant une meilleure expérience aux développeurs
  5. Coût d'exécution nul : toutes les vérifications ont lieu au moment de la compilation, sans surcharge d'exécution

Conclusion

L'utilisation de types non constructibles avec des messages d'erreur personnalisés est un modèle puissant pour créer des contraintes de type auto-documentées. Il exploite le système de types de TypeScript pour fournir des conseils clairs au moment de la compilation, aidant ainsi les développeurs à détecter et à résoudre les problèmes avant qu'ils ne deviennent des problèmes d'exécution.

Ce modèle est particulièrement utile lors de la construction de systèmes de types complexes où certaines combinaisons devraient être invalides. En rendant les états invalides irreprésentables et en fournissant des messages d'erreur clairs, nous pouvons créer un code TypeScript plus maintenable et plus convivial pour les développeurs.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn