Tipado seguro en Javascript

17 Oct 2016

Introducción

Un tema recurrente cuando hablamos de las características del lenguaje Javascript, es su tipado blando. Para muchos, esto es una funcionalidad interesante que permite flexibilidad en el código, mientras que para otros, es un problema grave que exige de una especial atención para evitar errores potenciales.

No vamos a discutir en este artículo si esto es o no un problema. En lugar de ello, lo que vamos es a ofrecer un marco en el que podamos trabajar cómodamente sin preocuparnos de los tipos.

El problema

Como bien sabemos, históricamente Javascript se ha caracterizado por un tipado blando (o por un no-tipado). Esto quiere decir que, a la hora de declarar variables, no necesitamos indicar el tipo al que pertenecen siendo el propio intérprete el que lo establece por nosotros:

var str = 'Hello World',
    number = 123,
    strTwo = '456',
    arr = [];
 
console.info( typeof str ); // string
console.info( typeof number ); // number
console.info( typeof strTwo ); // string
console.info( typeof arr ); // object

NOTA: de momento obviaremos que un array es tratado por el intérprete como un ‘objeto’. Más adelante, buscaremos algo más de precisión.

Los lenguajes de programación no tipados o débilmente tipados no controlan los tipos de las variables que declaran, de este modo, es posible usar variables de cualquier tipo en un mismo escenario.

Wikipedia, Tipado fuerte

Esta característica puede ser interesante al programar ya que permite una flexibilidad de la que carece el tipado fuerte (aquel que exige que indiquemos el tipo de valor que podemos asignar a una variable). Esa adaptabilidad la ofrece el propio lenguaje de forma automática o desatendida mediante un mecanismo interno que conocemos como coerción gracias al cual, cuando trabajamos con valores de tipos diferentes, delegamos en el intérprete la conversión.

En Ciencias de la Computación, la conversión de tipos y la coerción son diferentes formas de, explícita o implícitamente, cambiar la entidad de un tipo de datos en otro. Mientras que la primera, la conversión, es un cambio realizado en el código de forma explícita por parte del programador, la coerción es un proceso realizado por el compilador de forma autónoma e implícita.

Un ejemplo natural de coerción sería el realizado por Javascript al intentar sumar dos entidades que parecen números, siendo el primero un número real, y el segundo una cadena con aspecto de número:

var number = 123,
    str = '456';
 
var sum = number + str;
 
console.info( sum ); // 123456
console.info( typeof sum ); // string

En este caso, como tenemos dos tipos diferentes (un número y una cadena), el intérprete transforma el primero para poder operar entre ellos (concatenándolos). El resultado final es que todos los valores que intervienen en la operación (operandos y resultado) deben pertenecer al mismo tipo.

El mecanismo interno de conversión de tipos puede ser confuso si no se conoce el compilador:

var a = 2 + true,
    b = 2 + [],
    c = true + 'Foo',
    d = true + [],
    e = [] + [],
    f = false + {};
 
console.info( a, typeof a ); // 3 number
console.info( b, typeof b ); // 2 string
console.info( c, typeof c ); // trueFoo string
console.info( d, typeof d ); // true string
console.info( e, typeof e ); // string
console.info( f, typeof f ); // false [object Object] string

Como vemos, hay muchos casos que habría que tratar individualmente para ir viendo el porqué de su resultado. En este mismo blog podemos encontrar un artículo que trata todos estos casos más en profundidad.

Mundo ideal

En muchos escenarios, este tipado laxo no supone un problema. Entendemos que cuando tratamos de sumar números que han sido declarados como cadenas, el intérprete los va a concatenar sin más:

console.info( '123' + '456' ); // 123456

Resulta también natural que, en aquellos casos donde busquemos una suma, seamos nosotros desde el código quienes forcemos una conversión previa:

var x = '123',
    y = '456';
 
var x1 = parseInt( x, 10 ),
    y1 = parseInt( y, 10 );
 
var sum = x1 + y1;
 
console.info( sum, typeof sum ); // 579 number

Si conocemos las instrucciones para realizar las conversiones (aunque éstas no son siempre intuitivas), evitamos sustos.

NOTA: Nunca está de más recordar la necesidad de indicar la base, en este caso decimal, a la hora de utilizar la función parseInt.

Mundo real

El problema viene cuando olvidamos hacer esta conversión ‘segura’, o cuando simplemente no sospechamos que tenemos que hacerla… Es el escenario donde trabajamos con datos cuyo origen desconocemos o no son consistentes.

Pensemos en una respuesta que nos llega a través de una API externa. Generalmente, obtenemos un JSON en el que el tipado puede jugarnos malas pasadas:

var response = [
    {
        'id': 1,
        'title': 'Momo',
        'author': 'Michael Ende',
        'ISBN': '9788420471525',
        'published': 1986,
        'price': '29.95'
    }
];

Si una petición nos devuelve esos datos, sin más documentación que el ejemplo, podemos observar que:

  • El ID es un número.
  • El título y el autor, son cadenas.
  • El ISBN pese a que tiene un formato numérico, es una cadena también.
  • El año de publicación, sin embargo, es de nuevo un número.
  • El precio pese a tener aspecto de número es una cadena, que además está formateada con un punto para indicar ¿decimales?

Realizamos una segunda petición sobre la misma API, confiados de la estructura que presenta la respuesta. Y obtenemos:

var response = [
    {
        'id': 1,
        'title': 'Manuscrito Voynich',
        'author': 'Anonymous',
        'ISBN': false,
        'published': 'unknown',
        'price': false
    }
];

¡Los tipos de algunos valores han cambiado!

  • ID, título y autor mantienen el esquema anterior.
  • ISBN en esta ocasión viene como un booleano de tipo false.
  • La fecha de publicación, ahora no es un número, sino que viene como una cadena.
  • El precio ha pasado de ser una cadena a un valor booleano.

Podríamos decir que la API no es consistente, y que es un problema de diseño grave por parte de quien da el servicio al que estamos consultando. Y sería cierto, pero si tenemos que trabajar con ellos, hay que buscar una forma de asegurarnos la consistencia de los datos cuando los trabajamos. Esto, lamentablemente, suele ser el día a día del Mundo Real

La programación funcional

Otro marco de desarrollo donde los tipos son clave, es la programación funcional. Bajo este paradigma, y más concretamente dentro de la Teoría de categorías, necesitamos trabajar con un sistema que garantice la consistencia de los datos.

Sin embargo, en este contexto, la inferencia de tipos hace referencia a aquellos algoritmos que deducen automáticamente en tiempo de compilación – sin información adicional del programador, o bien con anotaciones parciales del programador – el tipo asociado con un uso de un objeto del programa. Un buen número de lenguajes de programación funcional permiten implantar inferencia de tipos (Haskell, OCaml, ML, etc).

Creando un marco seguro de tipos

Vistos los ejemplos anteriores, establecer un marco de trabajo donde trabajar con tipos sea seguro, resulta muy sencillo:

var typeOf = ( type ) => ( x ) => {
    if ( typeof x === type ) return x;
    throw new TypeError( "Error: " + type + " expected, " + typeof x + " given." );
};
 
var str = typeOf( 'string' ),
    num = typeOf( 'number' ),
    func = typeOf( 'function' ),
    bool = typeOf( 'boolean' );

La idea con este pequeño código es crear una serie de funciones auxiliares (str, num, func y bool) que aceptan solo argumentos de su correspondiente tipo. En caso de que el valor no corresponda, el intérprete nos arroja una excepción.

// Correct
console.info( str( 'Hello World' ) ); // Hello World
console.info( num( 123 ) ); // 123
console.info( func( function foo () {} ) ); // foo()
console.info( bool( true ) ); // true
console.info( bool( false ) ); // false
 
// Error / Exception
console.info( str( 123 ) ); // Error: string expected, number given
console.info( num( '123' ) ); // Error: number expected, string given
console.info( func( {} ) ); // Error: function expected, object given
console.info( bool( 'true' ) ); // Error: boolean expected, string given
console.info( bool( 'false' ) ); // Error: boolean expected, string given

En una implementación real de código, utilizariamos una estructura similar a la siguiente:

function sum ( x, y ) {
    return num( x ) + num( y );
}
 
console.info( sum( 2, 4 ) ); // 6
console.info( sum( 2, '3' ) ); // Error: number expected, string given

Un tipo especial de datos en Javascript: los objetos

Como sabemos, en Javacript, un objeto es todo aquello que no es una Primitiva (String, Number, Boolean o Symbol). Sin embargo, no tenemos forma de saber a qué tipo de objeto pertenece uno dado de forma nativa. Para ello, tenemos que crearnos nuestra propia fórmula:

var objectTypeOf = name => obj => {
    let toType = ( {} ).toString.call( obj ).match( /\s([a-z|A-Z]+)/ )[ 1 ].toLowerCase();
    if ( toType === name ) return obj;
 
    throw new TypeError( "Error: " + name + " expected, " + toType + " given." );
}
 
var obj = objectTypeOf( 'object' ),
    arr = objectTypeOf( 'array' ),
    date = objectTypeOf( 'date' );

Hemos añadido otras tres funciones auxiliares para comprobar el tipo de datos: obj, arr y date. De este modo, nuestras pruebas quedarían como sigue:

// Correct
console.info( obj( {} ) ); // Object {}
console.info( arr( [] ) ); // []
console.info( date( new Date() ) ); // Date { ... }
 
// Error / Exception
console.info( obj( [] ) ); // Error: object expected, array given.
console.info( arr( {} ) ); // Error: array expected, object given.
console.info( date( '13/11/2016' ) ); // Error: date expected, string given.

NOTA: Para realizar la comprobación exacta del tipo de dato, hemos utilizado una función que ya estudiamos en su día es este artículo: Cómo obtener el tipo de datos preciso de una variable en Javascript

Una implementación en código real de este método para comprobar objetos sería:

var map = function( fn, a ) {
    return arr( a ).map( func( fn ) );
}
 
map( String.toUpperCase, [ 'foo', 'bar' ] ); // [ 'FOO', 'BAR' ]
map( String.toUpperCase, 'Hello World' ); // Error: array expected, string given

En esta implementación personalizada de un método ‘map‘, vemos como podemos asegurarnos de que tanto el array como la función lo son. Sin duda, tenemos aquí una capa de seguridad esencial cuando la consistencia de los tipos es clave en nuestro flujo de trabajo.

Funciones auxiliares

En este caso sería interesante recalcar que, dado el valor inmutable de nuestras funciones auxiliares, lo correcto sería declararlas con ‘const’:

const str = typeOf( 'string' ),
    num = typeOf( 'number' ),
    func = typeOf( 'function' ),
    bool = typeOf( 'boolean' );

Como siempre, recordamos que si no usamos ‘const‘ durante los artículos, es para facilitar el estudio de los ejemplos con un copia/pega fácil en la consola del navegador. Y, para saber más sobre las constantes en Javascript, animamos a leer el artículo al respecto en este mismo blog: Las nuevas constantes en Javascript: explicación, ejemplos e inconsistencias.

Conclusión

Con este artículo, hemos creado una serie de funciones auxiliares que nos permiten monitorizar los tipos a los que pertenecen aquellos valores con los que trabajamos. Eso proporciona una capa de control interesante en aquellos componentes o programas donde necesitamos garantizar la consistencia de los mismos para operar de forma segura.

Existen numerosas bibliotecas de terceros, y frameworks, que incorporan un sistema similar. Metalenguajes como TypeScript es un claro ejemplo de esto. Sin embargo, cualquier sistema de este tipo exige una metodología de trabajo que no tiene porqué adaptarse a nuestros objetivos. Si nuestras necesidades pasan únicamente por controlar el tipado, una solución como la aquí aportada puede resultar suficiente sin necesidad de recurrir a herramientas externas o sistemas con una alta curva de entrada y aprendizaje.

Más:

Aún no tenemos debug!
Ningún marciano se ha comunicado.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *