Ramda y TypeScript: tipando view y lensPath

Edgar Rodríguez
codingedgar
Published in
4 min readAug 12, 2019

En esta serie estamos explorando como tipar la hermosa librería Ramda en TypeScript, quite a challenge, ya te habrás dado cuenta, joven explorador, que tipar es un poco enredado y Ramda, explorando fuertemente las ventajas funcionales de JavaScript es particularmente complicado de tipar.

TL;DR

¿Así rápido rápido? Todo lo que necesitas saber está en esta imagen:

Ok, un poco de background, ¿qué era Ramda?

Ramda

Ramda official logo

Según la página de Ramda: A practical functional library for JavaScript programmers.

En español: Una librería funcional práctica para programadores JavaScript.

Podríamos compararla con LoDash pero esta cumple con la especificación algebraica de JavaScript Fantasy Land, que es una guía para que todas las librerías que implementan conceptos de Programación Funcional puedan tener interoperabilidad de estructuras comunes. Ramda es el rock, sino lo has probado, just.. please, just.do.it.

Fantasy Land official logo

TypeScript

TypeScript official logo

Según la página de TypeScript: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

En español: Es un lenguaje de programación superconjunto (que tiene más características) de JavaScript que compila a simple JavaScript.

Podríamos decir que es como JavaScript pero tipado por nada más y nada menos que Microsft (y diseñado por el genial Anders Hejlsberg), su enfoque es permitir desarrollar grandes aplicaciones JavaScript de forma escalable y la mejor propiedad que tiene, es que todo código JavaScript es válido TypeScript 😘.

Conceptos de Ramda

View

Podemos entender “view” como una función que retorna una “vista” de la estructura de datos dada, haciendo foco en una parte con un lente.

Lentes

Los lentes son setters y getters, inmutables y componibles. Componibles en el sentido de que permiten actualizar estructuras de datos anidados. Inmutables en el sentido de que los setters retornan copias de toda la estructura de datos.

LensPath es una función de Ramda para declarar un lente con una ruta anidada, donde cada valor de tipo String representa una llave dentro de un objeto y cada valor de tipo Integer representa el índice dentro de un arreglo.

Ejemplo práctico

Vamos a comenzar primero por escribir un test (because TDD rocks 🤘), con el asombroso Jest por su puesto.

NestedType.ts

Vamos a declarar un tipo, para evitar repetirnos tanto, vamos a tener un objeto con 2 objetos anidados y el valor final es de tipo simbolo.

interface NestedType {
some: {
nested: {
value: Symbol
}
}
}

lens.test.ts

Va a tener un test para verificar que el resultado que arroja nuestro lente es igual al simbolo que tiene en su propiedad value.

import { lens } from './lens';

test('focus nested value', () => {
const unique = Symbol('nested');
expect(
lens({
some: {
nested: {
value: unique
}
}
})
).toBe(unique);
});

En el archivo lens.ts:

Para TypeScript tanto para view como lensPath le es imposible determinar el tipo que retorna o requiere, debido a la naturaleza polimórfica de estas funciones, por ende hay que escribir explícitamente los tipos de entrada y de salida de ambas funciones.

import { view, lensPath } from 'ramda';

export function lens(obj: NestedType): Symbol {
return view<Symbol, NestedType>(
lensPath<Symbol, NestedType>(['some', 'nested', 'value']),
obj
);
}

En el caso de View

En nuestro caso, entramos en una de las sobrecargas de las firmas más sencillos de view, donde toma un lente, que tenga el mismo tipo de entrada del view y arroje un tipo igual que el de salida de view.

A su vez lensPath necesita que declaremos el tipo de objeto de entrada y el de salida, y le proveamos un arreglo que utilizará para recorrer el objeto de entrada.

Esto le permite deducir a TypeScript que la salida de esta función es de tipo Symbol, pero no que la entrada es de tipo nestedType, por lo cual sino queremos que la firma de nuestra función sea:

function lens(obj: any): Symbol

Debemos de especificar también el tipo del objeto de entrada en la declaración de nuestra función externa, para que el tipo sea:

function lens(obj: NestedType): Symbol

Otros casos a considerar

En casos en que deseemos utilizar view en su forma curried y declarar:

const len0 = view<Symbol, NestedType>(
lensPath<Symbol, NestedType>(['some', 'nested', 'value'])
)

La firma que arroja es:

const lens0: view_manual_10<Symbol, NestedType>

Pero ésto igual nos da la seguridad que al llamar a len0(obj) TypeScript va a solicitar que dicho objeto sea nestedType.

Para terminar con la misma firma anterior podemos declarar :

export const len1: (obj: NestedType) => Symbol =
view<Symbol, NestedType>(
lensPath<Symbol, NestedType>(['some', 'nested', 'value'])
)

Esto nos devuelve nuestra típica función curried len1(obj) con todas las seguridades de tipeo y una firma un poco mas legible, aunque laboriosa.

export const len1: (obj: NestedType) => Symbol

Cheers ✌️🥂

--

--