jueves, 12 de marzo de 2020

Cuando tienes un martillo (TypeScript) ...

todo parecen clavos (tipos). 

Estaba buscando como se hacen uniones discriminadas y pattern matching con typescript y me he encontrado con artículo en un blog con una solución para "reducir la cantidad de código que tenemos que escribir en una funcion reduce de Redux sin comprometer la seguridad de tipos".

Traducido al castellano significa que así pulsamos menos teclas para construir la función reduce porque al estar fuertemente tipado salen las letritas y los cartelitos de autocompletado del intellisense del VS Code.
Para conseguirlo, el autor ha escrito el siguiente código:

enum ActionTypes {
  REQUEST_SUCCESS = "REQUEST_SUCCESS",
  REQUEST_FAILURE = "REQUEST_FAILURE",
}
 
type SFA<T, P> = { type: T, payload: P };
 
const createAction = <T extends ActionTypes, P>(
  type: T,
  payload: P
): SFA<T, P> => ({ type, payload });
 
const success = (payload: { items: Todo[] }) =>
  createAction(ActionTypes.REQUEST_SUCCESS, payload);
 
const failure = (payload: { reason: string }) =>
  createAction(ActionTypes.REQUEST_FAILURE, payload);
 
const actions = { success, failure };
 
type Action = ReturnType<typeof actions[keyof typeof actions]>;

1. Define un enumerado con los tipos de acciones.
2. Define un tipo genérico que contiene un tipo y un payload.
3. Define una función genérica que crea el tipo genérico anterior (2) restringiendo la entrada de genéricos a los tipos de acción definidos (1)
4. Define una función por cada tipo de acción; que construye el tipo genérico (2) usando la función genérica (3) y pasando le estáticamente uno de los tipos de acción definidos (1) y el formato del payload.
5. Define una clase que contiene las funciones constructoras (4).
6. Define una unión discriminada de tipos recorriendo los tipos de retorno de las funciones constructoras almacenadas en la clase anterior (5)

ufff... ha costado un ligero esfuerzo seguir el hilo del tema hasta llegar a entender de principio a fin la intención del programador.

y como se usa:

type Todo = { id: string };
 
type State = { items: Todo[], error: string };
 
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case ActionTypes.REQUEST_SUCCESS:
      return { ...state, items: action.payload.items, error: "" };
    case ActionTypes.REQUEST_FAILURE:
      return { ...state, items: [], error: action.payload.reason };
  }
  return state;
}

Al final termina con un switch case teniendo que preguntar por cada tipo definido en el enumerado; como se ha hecho en programación imperativa toda la vida...

Todo esto para que en el intellisense del editor de código le salga payload.items o payload.reason en unas cajitas flotantes...

Tengo que reconocer que el autor tiene que tener bastantes tablas, conocimientos del lenguaje y ser bastante inteligente para llegar a esta complicada solución pero quizás no ha sido suficientemente sagaz como para darse cuenta que es innecesaria y enrevesada.

Ahora pongamos mi implementación orientada a programación funcional sin tipos con javaScript sin florituras:

function Action(discriminator) {
  return {
    match: (cases) => discriminator(cases)
  };
}
 
const ActionDiscriminatedUnion = {
  fromInsert: (payload) => Action((cases) => cases.insert(payload)),
  fromUpdate: (payload) => Action((cases) => cases.update(payload)),
  fromDelete: (payload) => Action((cases) => cases.delete(payload))
};

1) La funcion Action retorna un objeto que ejecuta la función discriminadora que se le pase por parámetro.
2) ActionDiscrimitatedUnion retorna una Accion (1) con la función discriminadora que le toque.
3) Ya está... no hay nada más que hacer. Va como la seda y es más sencillo que un botijo.

y como se usa:

const state = { items: [123] };
const someUnknowAction = ActionDiscriminatedUnion.fromInsert({ items: [456] });
 
function reducer(state, someUnknowAction) {
 return someUnknowAction.match({
    insert: (payload) => applyInsert(state, payload),
    update: (payload) => applyUpdate(state, payload),
    delete: (payload) => applyDelete(state, payload)
  });
}

Comparen ustedes....

En mi opinión el autor ha caído en el sesgo cognitivo de que la solución a un puzle; si es muy compleja, tiene que ser la mejor porque requiere grandes conocimientos, entendimiento y soltura con las herramientas disponibles para solucionar el puzle. Pero no ha caído en que el puzle se lo ha montado él solito y que este no es necesario que exista; por lo que no es necesario una compleja solución a un puzle que podemos quitar de en medio si se hacen bien las cosas.










2 comentarios:

  1. Nunca creí que encontraría un blog de blogger vivo.

    ResponderEliminar
    Respuestas
    1. XD la pereza que me da ponerme a migrarlo puede con todo. En casa de herrero cuchillo de palo...

      Eliminar