5
\$\begingroup\$

I was told to post my question here from a comment on my question at Stack Overflow..

With reference to my code below, I am trying to create a Map<string, Function> (eventHandlerMap in the code below) such that each of its keys is a string from (PortalEvent in the code below) Union type. The value of each property should be a function that must return an object of specific type (in this example one of UserComment or Author).

Objectives:

  1. If a new key is added to PortalEvent (without any other change in code), a compile time failure should enforce a handler function to be defined for that new key and eventHandlerMap should be updated.

  2. If Author or UserComment type is changed the above, a compile time failure should enforce changing the structure of returned object in the respective handler.

If it helps the types PortalEvent, UserComment and Author are 3rd-party library maintained types and I want to be able to update my code whenever a new version of 3rd-party library is used in the project.

Here is the code I have come up: Playground Link


// Typescript Version 5.1.6

// Code for which I have no control over 
// -------------------------------------

type PortalEvent = 'comment_created' | 'author_updated';

type UserComment = {
  id: string;
  content: string;
}

type Author = {
  id: string;
  name: string;
  country: string
}


// Code that I can change
// ----------------------

// Define the types necessary for defining object eventHandlerMap
type HandlerReturnType = {
  'comment_created': UserComment,
  'author_updated': Author,
}

type HandlerArgType = {
  'comment_created': { arg1: number, arg2: string; },
  'author_updated': { arg1: string },
}

type THandlerMap<A extends {[j in PortalEvent]: unknown}, R extends {[j in PortalEvent]: unknown}> = {
  [key in PortalEvent]: (arg: A[key]) => R[key] ;
};

// Define the Event handler map
const eventHandlerMap: THandlerMap<HandlerArgType, HandlerReturnType> = {
  'comment_created': commentCreatedHandler,
  'author_updated': authorUpdatedHandler,
}


function commentCreatedHandler(args: HandlerArgType['comment_created']): UserComment {
  let id: string = '', content: string = '';

  // ... code to initialize id and content properties

  return {
      id,
      content,
  };
}

function authorUpdatedHandler(args: HandlerArgType['author_updated']): Author {
  let id: string = '', name: string = '', country : string = '';

  // ... code to initialize id, name and country properties

  return {
      id,
      name,
      country
  };
}

The code above achieves the listed objectives. Is there a better way of doing this?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

breaking the build

Thank you for very explicitly stating two objectives. They are testable Requirements.

Is there a better way? How about "another" way.

Certainly type safety is one approach to keeping caller and callee consistent. I invite you to also consider runtime approaches.

  1. An if debug flag can trigger extra checking of variables.
  2. A call to a pre-condition helper predicate can verify an object has desired number of keys, and that each mandatory key is actually present.
  3. Similarly a call to a post-condition helper can "keep our function honest", verifying it actually returns what it promised.

Often we'll need to implement (2.) anyway, even with compiler checks, when accepting JSON data of uncertain shape from a recently updated API endpoint.

An automated e2e integration test can arrange for such checking to happen with a debug flag set. Clearly a failed assertion would break the build, allowing us to fix the problem. Code coverage measurements are a good way to verify we've tested what we intended to test, and we can arrange for uncovered target code to also break the build, giving us a chance to write an additional test.

consistent types

It's not clear that this would even compile.

type UserComment = {
  id: string;
  content: string;
}
...
type HandlerArgType = {
  'comment_created': { arg1: number, arg2: string; },

We can invent much better parameter names than arg1 and arg2. It looks like id wants to be a number. And the 'author_updated' handler probably needs multiple args.

I am not a compiler. I can see your intent. It looks fine. Do test it, to verify that both of your stated goals work out, that mismatches really do produce fatal compiler diagnostics.

audit trail

Consider adding created and updated fields to records. It will help you figure out what people did, what code did, and whether it was intended. An indexed timestamp column can also be a great way for processing recent records, such as synchronizing datastores across hosts, or undoing harm done by a code defect that snuck into yesterday's release.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.