Des problèmes de types, avec Typescript

Et peut être bien un problème de conception

a marqué ce sujet comme résolu.

Hello tout le monde,

J’ai un soucis avec TypeScript et la gestion des types. J’ai l’impression d’être face à un problème très simple et pourtant il me semble vraiment complexe en terme de résolution des types etc.

Commençons par le code:

interface MessageInterface {
    _type: string;
}

interface ErrorMessage extends MessageInterface {
    error: string;
}

// C'est pour l'exemple mais imaginez 30 types différents ici
export enum MessageType {
  CONNECT,
  ERROR,
  CHAT
}


const messagesCallbacks: Map<MessageType, (message: MessageInterface) => void> = new Map();
function listenOn (messageType: MessageType, callback: (message: MessageInterface) => void): void {
    messagesCallbacks.set(messageType, callback);
}

listenOn(MessageType.ERROR, (message: ErrorMessage) => {
    console.error(message.error);
});

function receiveMessage(message: string): void {
    const decoded = JSON.parse(message);
    if (decoded._type === MessageType.ERROR && messagesCallbacks.has(MessageType.ERROR)) {
        messagesCallbacks.get(MessageType.ERROR)!(decoded);
    }
}

Cliquez ici pour voir live.

Donc j’ai deux questions:

  1. Est-ce que vous avez une solution simple à ce problème de typage ?
  2. Est-ce que j’ai un problème de design et il est possible de faire quelque-chose de similaire différemment en TS ? (après tout, c’est loin d’être mon langage de prédilection donc je peux rater qqch) Cela en prenant en compte le fait que j’ai 30 types de messages différents ofc.

Merci d’avance pour vos avis :)

D’après le type de messagesCallbacks, les callbacks peuvent recevoir n’importe quel objet dont le type étend MessageInterface. Cela veut donc dire qu’il est permit d’écrire le code suivant :

const msg = { _type: "foo", hello: "bar" } // msg étend MessageInterface
messagesCallbacks.get(MessageType.ERROR)!(msg)

Si ton callback définit en ligne 22 était accepté par le compilateur, tu aurais dans ce cas une erreur à l’exécution, car le champ error est inexistant dans sur objet msg passé en paramètre. L’erreur relevée par le compilateur permet justement de prévenir ce problème.

Une solution possible serait de définir un type guard (par exemple isErrorMessage), et de l’appeler dans le callback afin de vérifier que le message est bien dans le type désiré.

function isErrorMessage(msg: MessageInterface): msg is ErrorMessage {
    return !!(msg as ErrorMessage).error
}

listenOn(MessageType.ERROR, message => {
    if (isErrorMessage(message))
        // `message` est donc bien un ErrorMessage
        console.error(message.error);
    else
        throw new Error("Le message reçu n'est pas un message d'erreur");
});

Pour moi le plus propre serait d’avoir des types différents pour les messages portant des informations différentes, et donc des callbacks qui attendent en entrée des types différents.

Si tu as besoin de pouvoir "set" dynamiquement des callbacks pour les messages, ça suggèrerait d’avoir un callback modifiable pour chaque type de message (donc 30 callbacks modifiable au lieu d’une seule table à 30 entrées). Mais est-ce que tu en as vraiment besoin, ou tu pourrais écrire ton code sans, par exemple en matchant sur le type de message dans receiveMessage et en dispatchant directement sur l’un des 30 gestionnaires que tu as dans ton code ?

Disclaimer : je ne connais pas TypeScript.

Avec ton code actuel, on dirait que tu veux créer une hiérarchie de type par héritage, sauf que tes sous-types ne sont pas interchangeables avec le parent et ça ne peut donc pas marcher.

On pourrait unifier les types en faisant en sorte que les types se décrivent eux-mêmes (quelque chose du genre message.log()), et dans ce cas, tu aurais juste à implémenter différents comportement pour chaque type et ta Map ne servirait probablement plus à rien parce que le dispatch serait fait naturellement par ta hiérarchie de type. Le type parent pourrait même implémenter le cas d’erreur "type de message inconnu" si tu reçois n’importe quoi en entrée.

Si tu veux garder des fonctions entièrement flexibles pour chaque type, comme actuellement, je ne vois pas d’autre possibilité que de caster à un moment donné. A priori dans receiveMessage, vu que tu dois de toute façon gérer les cas où on tu reçois n’importe quoi dans le JSON ?

Le cast c’est l’option que je souhaite éviter, et c’est pour ça que je me pose la question de l’architecture.

Cela étant dit, le truc de base avec 30 propriétés différentes pour refléter la réalité ça fonctionne (et c’est le premier truc auquel j’ai pensé), et au final c’est plus maintenable que ça en a l’air. Maintenant cette solution ne m’enchante pas - je n’ai pas vraiment d’argument contre car toutes les autres solutions que j’ai en tête sont plus complexes ou moins fiables.

Je laisse le topic ouvert car en réalité je ne suis pas bloqué. C’est plutôt un sujet intéressant je trouve, si quelqu’un a des idées.

Note: je vous partagerai plus tard une expérimentation si elle fonctionne. (il me faut juste un peu de temps - que je n’ai pas là - pour la tester)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte