TypeScript para programadores Funcionales

TypeScript para programadores funcionales

¿Qué es la Programación Funcional?

Programación Funcional: es el paradigma de programación que esta teniendo un auge hoy en dia, como su nombre lo device es un tipo de programacion en la que se desarrolla en base a funciones, y lo importante de una funcion es "¿Qué hace?" y NO "¿Cómo lo hace?". Esto quiere decir que nosotros expresaremos nuestra lógica sin describir controles de flujo; no usaremos ciclos o condicionales. Cuando nos encontramos desarrollamos software utilizando este paradigma, estaremos trabajando principalmente con funciones, evitaremos los datos mutables, así como el hecho de compartir estados entre funciones.

TypeScript comenzó su vida como un intento de llevar los tipos tradicionales orientados a objetos a JavaScript para que los programadores de Microsoft pudieran llevar los programas tradicionales orientados a objetos a la web. A medida que se ha desarrollado, el sistema de tipos de TypeScript ha evolucionado para modelar el código escrito por JavaScripters nativos. El sistema resultante es poderoso, interesante y desordenado.

Esta introducción está diseñada para programadores de Haskell o Maching Learning (ML) que quieran aprender TypeScript, también describe en qué se diferencia el sistema de tipos de TypeScript del sistema de tipos de Haskell. También describe características únicas del sistema de tipos de TypeScript que surgen de su modelado de código JavaScript.

Esta introducción no cubre la programación orientada a objetos.En la práctica,los programas orientados a objetos en TypeScript son similares a los de otros lenguajes populares con características OO.

Prerrequisitos

Como se mencionó, esta seccion esta creada para personas con conocimientos de:

  • Cómo programar en JavaScript,las partes buenas.
  • Sintaxis de tipo de un lenguaje descendiente de C.
  • Conocimientos basicos de programación en Haskell

Tranquilo, sino tienes conocimientos sobre estos prerrequisitos, aunque este apartado este orientado a personas con algunos conocimientos, te dejo algunos enlaces a cursos para que puedas aprender un poco más sobre estos temas:

El lenguaje de programación C ++ es un buen lugar para aprender sobre la sintaxis de tipo de estilo C. A diferencia de C ++, TypeScript usa tipos de sufijo, así: x: string en lugar de string x .

Conceptos que no están en Haskell

Tipos Incorporados

JavaScript define 8 tipos incorporados:

Tipo Explicación
Numbre un punto flotante IEEE 754 de doble precisión.
String una cadena UTF-16 inmutable.
BigInt enteros en el formato de precisión arbitraria.
Boolean true y false.
Symbol un valor único que suele utilizarse como clave.
Null equivalente al tipo de unidad.
Undefined También es equivalente al tipo de unidad.
Object similares a los registros.

TypeScript tiene tipos primitivos correspondientes para los tipo incorporados

  • number
  • string
  • bigint
  • boolean
  • symbol
  • null
  • undefined
  • object

Otros tipos importantes de TypeScript

Tipo Explicación
unknown el tipo superior.
never el tipo de fondo.
object literal por ejemplo, { property: Type }
void un subtipo de undefined destinado a usarse como tipo de retorno.
T[] matrices mutables, también escritas Array
[T, T] tuplas,que son de longitud fija pero mutable
(t: T) => U functions

Notas:

  1. La sintaxis de la función incluye nombres de parámetros. ¡Es muy difícil acostumbrarse a esto!
    
        let fst: (a: any, b: any) => any = (a, b) => a;
    
        // o más precisamente:
    
        let fst: (a: T, b: U) => T = (a, b) => a;
                            
  2. La sintaxis de tipo de literal de objeto refleja de cerca la sintaxis de valor de literal de objeto:
    
        let o: { n: number; xs: object[] } = { n: 1, xs: [] };
                            
  3. [T, T]es un subtipo de T[]. Esto es diferente a Haskell, donde las tuplas no están relacionadas con las listas.

Tipos en caja

JavaScript tiene equivalentes en caja de tipos primitivos que contienen los métodos que los programadores asocian con esos tipos. TypeScript refleja esto con, por ejemplo, la diferencia entre el tipo primitivo number y el tipo en caja Number. Los tipos en caja rara vez se necesitan, ya que sus métodos devuelven primitivas.


        (1).toExponential();
        // equivalente a
        Number.prototype.toExponential.call(1);
                  

Tenga en cuenta que llamar a un método en un literal numérico requiere que esté entre paréntesis para ayudar al analizador.

Tipeado gradual

TypeScript usa el tipo any siempre que no puede decir cuál debería ser el tipo de una expresión. En comparación con Dynamic, llamar a any un tipo es una exageración. Simplemente apaga el verificador de tipo donde aparece. Por ejemplo, puede insertar cualquier valor en un any[] sin marcar el valor de ninguna manera:


    // with "noImplicitAny": false in tsconfig.json, anys: any[]
    const anys = [];
    anys.push(1);
    anys.push("oh no");
    anys.push({ anything: "goes" });
                  

Y puede usar una expresión de tipo any en cualquier lugar:


    anys.map(anys[1]); // oh no, "oh no" is not a function
                  

any también es contagioso: si inicializa una variable con una expresión de tipo any , la variable también tiene el tipo any .


    let sepsis = anys[0] + anys[1]; // this could mean anything
                  
Para obtener un error cuando TypeScript produce un any , use "noImplicitAny": true o "strict": true en tsconfig.json .

Tipificación estructural

El tipado estructural es un concepto familiar para la mayoría de los programadores funcionales,aunque Haskell y la mayoría de los ML no están tipados estructuralmente.Su forma básica es bastante simple:

    // @strict: falso
    let o = { x: "hi", extra: 1 }; // ok
    let o2: { x: string } = o; // ok
                  
Y puede usar una expresión de tipo en anycualquier lugar: Aquí, el objeto literal { x: "hi", extra: 1 } tiene un tipo de literal coincidente { x: string, extra: number }. Ese tipo es asignable a { x: string } ya que tiene todas las propiedades requeridas y esas propiedades tienen tipos asignables. La propiedad adicional no evita la asignación, solo la convierte en un subtipo de { x: string }. Los tipos con nombre simplemente dan un nombre a un tipo; para propósitos de asignabilidad, no hay diferencia entre el alias de tipo One el tipo de interfaz a Two continuación. Ambos tienen una propiedad p: string . (Sin embargo, los alias de tipo se comportan de manera diferente a las interfaces con respecto a las definiciones recursivas y los parámetros de tipo).

    type One = { p: string };
    interface Two {
        p: string;
    }
    class Three {
        p = "Hello";
    }
 
    let x: One = { p: "hi" };
    let two: Two = x;
    two = new Three();
                  

Uniones

En TypeScript, los tipos de unión no están etiquetados. En otras palabras, no son sindicatos discriminados como data en Haskell. Sin embargo, a menudo puede discriminar tipos en una unión utilizando etiquetas integradas u otras propiedades.

function start(
    arg: string | string[] | (() => string) | { s: string }
): string {
    // esto es muy común en JavaScript
    if (typeof arg === "string") {
        return commonCase(arg);
    } else if (Array.isArray(arg)) {
        return arg.map(commonCase).join(",");
    } else if (typeof arg === "function") {
        return commonCase(arg());
    } else {
        return commonCase(arg.s);
    }
 
    function commonCase(s: string): string {
        // finalmente, simplemente convierte una cadena en otra cadena
        return s;
    }
}
                  
string, Array Y Function se han incorporado en los predicados de tipo, convenientemente dejando el tipo de objeto para la elserama. Sin embargo, es posible generar uniones que sean difíciles de diferenciar en tiempo de ejecución. Para un código nuevo, es mejor crear solo sindicatos discriminados. Los siguientes tipos tienen predicados integrados:
Tipo Predicado
string typeof s === "string"
number typeof n === "number"
bigint typeof m === "bigint"
boolean typeof b === "boolean"
symbol typeof g === "symbol"
undefined typeof undefined === "undefined"
function typeof f === "function"
array Array.isArray(a)
object typeof o === "object"
Tenga en cuenta que las funciones y los arrays son objetos en tiempo de ejecución,pero tienen sus propios predicados.

Intersecciones

Además de las uniones, TypeScript también tiene intersecciones:

    type Combined = { a: number } & { b: string };
    type Conflicting = { a: number } & { a: string };
                    
Combined tiene dos propiedades, a y b , tal como si hubieran sido escritos como un objeto de tipo literal. La intersección y la unión son recursivas en caso de conflictos, entonces Conflicting.a: number & string .

Tipos de Unidad

Los tipos de unidad son subtipos de tipos primitivos que contienen exactamente un valor primitivo. Por ejemplo, la cadena "foo" tiene el tipo "foo" . Dado que JavaScript no tiene enumeraciones integradas, es común usar un conjunto de cadenas conocidas en su lugar. Las uniones de tipos de cadenas literales permiten que TypeScript escriba este patrón:

declare function pad(s: string, n: number, direction: "left" | "right"): string;
pad("hi", 10, "left");
                  

Cuando es necesario, el compilador amplía (convierte a un supertipo) el tipo de unidad al tipo primitivo, como "foo" a string . Esto sucede cuando se usa la mutabilidad, lo que puede dificultar algunos usos de variables mutables:


    let s = "right";
    pad("hi", 10, s); // error: 'string' is not assignable to '"left" | "right"'
                  
Así es como ocurre el error:
  • "right": "right"
  • s: string porque "right" se ensancha a string en la asignación a una variable mutable.
  • string no se puede asignar a "left" | "right"

Puede solucionar esto con una anotación de tipo para s, pero eso a su vez evita asignaciones s de variables que no son de tipo "left" | "right".


  let s: "left" | "right" = "right";
  pad("hi", 10, s);
                  

Conceptos similares a Haskell

Tipeado contextual

TypeScript tiene algunos lugares obvios donde puede inferir tipos, como declaraciones de variables:


      let s = "I'm a string!";
                

Pero también infiere tipos en algunos otros lugares que puede que no espere si ha trabajado con otros lenguajes de sintaxis C:


  declare function map(f: (t: T) => U, ts: T[]): U[];
  let sns = map((n) => n.toString(), [1, 2, 3]);
                

Aquí, n: number en este ejemplo también, a pesar de que T> y U no se han inferido antes de la llamada. De hecho, después de que [1,2,3] se ha usado para inferir T: number, el tipo de retorno de n => n.toString() se usa para inferir U=string, lo sns que hace que tenga el tipo string[].

Tenga en cuenta que la inferencia funcionará en cualquier orden, pero intellisense solo funcionará de izquierda a derecha, por lo que TypeScript prefiere declarar el map a con la matriz primero:


  declare function map(ts: T[], f: (t: T) => U): U[];
                

La tipificación contextual también funciona de forma recursiva a través de literales de objeto y en tipos de unidades que de otro modo se inferirían como string o number. Y puede inferir tipos de retorno del contexto:


    declare function run(thunk: (t: T) => void): T;
    let i: { inference: string } = run((o) => {
      o.inference = "INSERT STATE HERE";
    });
                

Se determina que el tipo de o es { inference: string } porque

  • Los inicializadores de declaración se escriben contextualmente por el tipo de declaración: { inference: string } .
  • El tipo de retorno de una llamada usa el tipo contextual para inferencias, por lo que el compilador infiere que T={ inference: string } .
  • Las funciones de flecha usan el tipo contextual para escribir sus parámetros, por lo que el compilador da o: { inference: string } .

Y lo hace mientras escribe, de modo que después de escribir o. , obtiene finalizaciones para la inference propiedades , junto con cualquier otra propiedad que tenga en un programa real. En conjunto, esta característica puede hacer que la inferencia de TypeScript se parezca un poco a un motor de inferencia de tipo unificador, pero no lo es.

Escriba alias

Los alias de tipo son meros alias, como typeen Haskell. El compilador intentará utilizar el nombre de alias siempre que se haya utilizado en el código fuente, pero no siempre lo consigue.


  type Size = [number, number];
  let x: Size = [101.1, 999.9];
                

El equivalente más cercano a newtype es una intersección etiquetada :


  type FString = string & { __compileTimeOnly: any };
                

Una FString es como una cadena normal, excepto que el compilador cree que tiene una propiedad llamada __compileTimeOnly que en realidad no existe. Esto significa que FString todavía se puede asignar a una string , pero no al revés.

Uniones discriminadas

El equivalente más cercano a dataes una unión de tipos con propiedades discriminantes, normalmente llamadas uniones discriminadas en TypeScript:


  type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "square"; x: number }
    | { kind: "triangle"; x: number; y: number };
                

A diferencia de Haskell, la etiqueta, o discriminante, es solo una propiedad en cada tipo de objeto. Cada variante tiene una propiedad idéntica con un tipo de unidad diferente. Este sigue siendo un tipo de unión normal; el líder | es una parte opcional de la sintaxis del tipo de unión. Puede discriminar a los miembros de la unión utilizando código JavaScript normal:


type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };
 
function area(s: Shape) {
  if (s.kind === "circle") {
    return Math.PI * s.radius * s.radius;
  } else if (s.kind === "square") {
    return s.x * s.x;
  } else {
    return (s.x * s.y) / 2;
  }
}
                

Tenga en cuenta que se infiere que el tipo de area de retorno es un number porque TypeScript sabe que la función es total. Si alguna variante no está cubierta, el tipo de area devuelta será number | undefined lugar.

Además,a diferencia de Haskell,las propiedades comunes aparecen en cualquier unión,por lo que puede ser útil discriminar varios miembros de la unión:


  function height(s: Shape) {
    if (s.kind === "circle") {
      return 2 * s.radius;
    } else {
      // s.kind: "square" | "triangle"
      return s.x;
    }
  }
                

Tipos de Parametro

Como la mayoría de los lenguajes descendientes de C,TypeScript requiere la declaración de parámetros de tipo:


function liftArray(t: T): Array {
  return [t];
}
                

No hay ningún requisito de mayúsculas y minúsculas,pero los parámetros de tipo son convencionalmente letras mayúsculas simples.Los parámetros de tipo también pueden ser restringidos a un tipo,lo que se comporta un poco como las restricciones de clase de tipo:


function firstish(t1: T, t2: T): T {
  return t1.length > t2.length ? t1 : t2;
}
                

TypeScript normalmente puede inferir los argumentos de tipo de una llamada basándose en el tipo de los argumentos,por lo que los argumentos de tipo normalmente no son necesarios.

Debido a que TypeScript es estructural, no necesita tanto parámetros de tipo como sistemas nominales. Específicamente, no son necesarios para hacer que una función sea polimórfica. Los parámetros de tipo solo deben usarse para propagar información de tipo, como restringir los parámetros para que sean del mismo tipo:


function length< T extends ArrayLike < unknown > >(t: T): number {}

function length(t: ArrayLike): number {}
                

En la primera length , T no es necesaria; observe que solo se hace referencia a él una vez, por lo que no se usa para restringir el tipo de valor de retorno u otros parámetros.

Tipos de Clase Superior

TypeScript no tiene tipos de clase superior,por lo que lo siguiente no es legal:


function length< T extends ArrayLike < unknown >, U >(m: T< U >) {}
                  

Programación sin puntos

La programación sin puntos (uso intensivo de currying y composición de funciones) es posible en JavaScript, pero puede ser detallada. En TypeScript, la inferencia de tipos a menudo falla para los programas sin puntos, por lo que terminará especificando parámetros de tipo en lugar de parámetros de valor. El resultado es tan detallado que normalmente es mejor evitar la programación sin puntos.

Sistema de Módulos

La sintaxis del módulo moderno de JavaScript es un poco como la de Haskell, excepto que cualquier archivo con importo exportes implícitamente un módulo:


import { value, Type } from "npm-package";
import { other, Types } from "./local-package";
import * as prefix from "../lib/third-package";
                  

También puede importar módulos commonjs, módulos escritos con el sistema de módulos de node.js:


import f = require("single-function-package");
                  

Puede exportar con una lista de exportación:


  export { f };

  function f() {
    return g();
  }
  function g() {} // g no se exporta
                  

O marcando cada exportación individualmente:


  export function f { return g() }
  function g() { }
                  

Este último estilo es más común,pero ambos están permitidos,incluso en el mismo archivo.

readonly y const

En JavaScript, la mutabilidad es la predeterminada, aunque permite declaraciones de variables const para declarar que la referencia es inmutable. El referente sigue siendo mutable:


  const a = [1, 2, 3];
  a.push(102); // ):
  a[0] = 101; // D:
                  

TypeScript también tiene un modificador de readonly para las propiedades.


  interface Rx {
    readonly x: number;
  }
  let rx: Rx = { x: 1 };
  rx.x = 12; // error
                  

También se envía con un tipo asignado Readonly que hace que todas las propiedades sean de readonly :


  interface X {
    x: number;
  }
  let rx: Readonly = { x: 1 };
  rx.x = 12; // error
                  

Y tiene un tipo específico ReadonlyArray que elimina los métodos que afectan a los lados y evita la escritura en los índices de la matriz, así como una sintaxis especial para este tipo:


  let a: ReadonlyArray = [1, 2, 3];
  let b: readonly number[] = [1, 2, 3];
  a.push(102); // error
  b[0] = 101; // error
                  

También puede utilizar una const-assertion,que opera sobre arrays y objetos literales:


  let a = [1, 2, 3] as const;
  a.push(102); // error
  a[0] = 101; // error
                  

Sin embargo,ninguna de estas opciones es la predeterminada,por lo que no se utilizan sistemáticamente en el código TypeScript.

Volver arriba

Autores: Grupo 07 de TPI115 - UES