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 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 programadores nativos de JavaScript. El sistema resultante es potente, interesante y complejo.
Esta introducción está diseñada para programadores funcionales de Haskell o ML que quieran aprender TypeScript. Describe cómo el sistema de tipos de TypeScript difiere del sistema de tipos de Haskell. También describe características únicas del sistema de tipos de TypeScript que surgen de su modelado del 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 de POO.
Prerrequisitos
En esta introducción, asumo que sabes lo siguiente:
- Cómo programar en JavaScript, las partes buenas.
- Sintaxis de tipos de un lenguaje descendiente de C.
Si necesitas aprender las partes buenas de JavaScript, lee JavaScript: The Good Parts. Es posible que puedas omitir el libro si sabes cómo escribir programas en un lenguaje de ámbito léxico y llamada por valor con mucha mutabilidad y no mucho más. R4RS Scheme es un buen ejemplo.
The C++ Programming Language es
un buen lugar para aprender sobre la sintaxis de tipos estilo C. A diferencia de C++,
TypeScript usa tipos posfijos, 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 |
|---|---|
Number |
un número de punto flotante de doble precisión IEEE 754. |
String |
una cadena UTF-16 inmutable. |
BigInt |
enteros en formato de precisión arbitraria. |
Boolean |
true y false. |
Symbol |
un valor único usualmente usado como clave. |
Null |
equivalente al tipo unitario (unit). |
Undefined |
también equivalente al tipo unitario (unit). |
Object |
similar a los registros (records). |
Consulta la página de MDN para más detalles.
TypeScript tiene tipos primitivos correspondientes para los tipos incorporados:
numberstringbigintbooleansymbolnullundefinedobject
Otros tipos importantes de TypeScript
| Tipo | Explicación |
|---|---|
unknown |
el tipo superior (top type). |
never |
el tipo inferior (bottom type). |
| literal objeto | ej. { propiedad: Tipo } |
void |
para funciones sin valor de retorno documentado |
T[] |
arrays mutables, también escrito Array<T> |
[T, T] |
tuplas, que son de longitud fija pero mutables |
(t: T) => U |
funciones |
Notas:
-
La sintaxis de función incluye nombres de parámetros. ¡Esto es bastante difícil de acostumbrarse!
tslet fst: (a: any, b: any) => any = (a, b) => a;// o más precisamente:let fst: <T, U>(a: T, b: U) => T = (a, b) => a; -
La sintaxis de tipo literal objeto refleja de cerca la sintaxis de valor literal objeto:
tslet o: { n: number; xs: object[] } = { n: 1, xs: [] }; -
[T, T]es un subtipo deT[]. Esto es diferente de Haskell, donde las tuplas no están relacionadas con las listas.
Tipos “en caja” (Boxed types)
JavaScript tiene equivalentes “en caja” (boxed) 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 son
necesarios, ya que sus métodos devuelven primitivos.
ts(1).toExponential();// equivalente aNumber.prototype.toExponential.call(1);
Nota que llamar a un método en un literal numérico requiere que esté entre
paréntesis para ayudar al analizador sintáctico (parser).
Tipado Gradual
TypeScript usa el tipo any siempre que no puede determinar cuál debería ser el tipo
de una expresión. Comparado con Dynamic, llamar a any un tipo
es una exageración. Simplemente desactiva el verificador de tipos
dondequiera que aparezca. Por ejemplo, puedes insertar cualquier valor en un
any[] sin marcar el valor de ninguna manera:
tsTry// con "noImplicitAny": false en tsconfig.json, anys: any[]constanys = [];anys .push (1);anys .push ("oh no");anys .push ({anything : "goes" });
Y puedes usar una expresión de tipo any en cualquier lugar:
tsanys.map(anys[1]); // oh no, "oh no" no es una función
any también es contagioso — si inicializas una variable con una
expresión de tipo any, la variable también tiene tipo any.
tslet sepsis = anys[0] + anys[1]; // esto podría significar cualquier cosa
Para obtener un error cuando TypeScript produce un any, usa
"noImplicitAny": true, o "strict": true en tsconfig.json.
Tipado 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 son tipados estructuralmente. Su forma básica es bastante simple:
ts// @strict: falselet o = { x: "hi", extra: 1 }; // oklet o2: { x: string } = o; // ok
Aquí, el literal objeto { x: "hi", extra: 1 } tiene un tipo 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 extra no previene la asignación, solo
lo 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 y el tipo de interfaz
Two a continuación. Ambos tienen una propiedad p: string. (Los alias de tipo
se comportan de manera diferente a las interfaces con respecto a las definiciones
recursivas y los parámetros de tipo, sin embargo.)
tsTrytypeOne = {p : string };interfaceTwo {p : string;}classThree {p = "Hello";}letx :One = {p : "hi" };lettwo :Two =x ;two = newThree ();
Uniones
En TypeScript, los tipos unión no están etiquetados. En otras palabras, no son
uniones discriminadas como data en Haskell. Sin embargo, a menudo puedes
discriminar tipos en una unión usando etiquetas incorporadas u otras propiedades.
tsTryfunctionstart (arg : string | string[] | (() => string) | {s : string }): string {// esto es súper común en JavaScriptif (typeofarg === "string") {returncommonCase (arg );} else if (Array .isArray (arg )) {returnarg .map (commonCase ).join (",");} else if (typeofarg === "function") {returncommonCase (arg ());} else {returncommonCase (arg .s );}functioncommonCase (s : string): string {// finalmente, solo convierte una cadena a otra cadenareturns ;}}
string, Array y Function tienen predicados de tipo incorporados,
dejando convenientemente el tipo objeto para la rama else. Es
posible, sin embargo, generar uniones que son difíciles de
diferenciar en tiempo de ejecución. Para código nuevo, es mejor construir solo
uniones discriminadas.
Los siguientes tipos tienen predicados incorporados:
| 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" |
Nota 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:
tsTrytypeCombined = {a : number } & {b : string };typeConflicting = {a : number } & {a : string };
Combined tiene dos propiedades, a y b, tal como si hubieran sido
escritas como un tipo literal objeto. La intersección y la unión son
recursivas en caso de conflictos, por lo que Conflicting.a: number & string.
Tipos Unitarios
Los tipos unitarios 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 incorporadas, es común usar un conjunto de
cadenas bien conocidas en su lugar. Las uniones de tipos literales de cadena permiten
a TypeScript tipar este patrón:
tsTrydeclare functionpad (s : string,n : number,direction : "left" | "right"): string;pad ("hi", 10, "left");
Cuando es necesario, el compilador amplía (widens) — convierte a un
supertipo — el tipo unitario al tipo primitivo, como "foo"
a string. Esto sucede cuando se usa mutabilidad, lo que puede obstaculizar algunos
usos de variables mutables:
tsTrylets = "right";Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.2345Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.pad ("hi", 10,); // error: 'string' no es asignable a '"left" | "right"' s
Así es como ocurre el error:
"right": "right"s: stringporque"right"se amplía astringen la asignación a una variable mutable.stringno es asignable a"left" | "right"
Puedes solucionar esto con una anotación de tipo para s, pero eso
a su vez impide asignaciones a s de variables que no sean de tipo
"left" | "right".
tsTrylets : "left" | "right" = "right";pad ("hi", 10,s );
Conceptos similares a Haskell
Tipado Contextual
TypeScript tiene algunos lugares obvios donde puede inferir tipos, como declaraciones de variables:
tsTrylets = "¡Soy una cadena!";
Pero también infiere tipos en algunos otros lugares que quizás no esperes si has trabajado con otros lenguajes de sintaxis C:
tsTrydeclare functionmap <T ,U >(f : (t :T ) =>U ,ts :T []):U [];letsns =map ((n ) =>n .toString (), [1, 2, 3]);
Aquí, n: number en este ejemplo también, a pesar del hecho de que T y U
no han sido inferidos antes de la llamada. De hecho, después de que [1,2,3] ha
sido usado para inferir T=number, el tipo de retorno de n => n.toString()
se usa para inferir U=string, haciendo que sns tenga el tipo
string[].
Nota que la inferencia funcionará en cualquier orden, pero intellisense solo
funcionará de izquierda a derecha, por lo que TypeScript prefiere declarar map con el
array primero:
tsTrydeclare functionmap <T ,U >(ts :T [],f : (t :T ) =>U ):U [];
El tipado contextual también funciona recursivamente a través de literales objeto, y
en tipos unitarios que de otro modo se inferirían como string o
number. Y puede inferir tipos de retorno desde el contexto:
tsTrydeclare functionrun <T >(thunk : (t :T ) => void):T ;leti : {inference : string } =run ((o ) => {o .inference = "INSERTAR ESTADO AQUÍ";});
El tipo de o se determina como { inference: string } porque
- Los inicializadores de declaración son tipados contextualmente por el
tipo de la 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 flecha usan el tipo contextual para tipar sus parámetros,
por lo que el compilador asigna
o: { inference: string }.
Y lo hace mientras escribes, de modo que después de escribir o., obtienes
autocompletado para la propiedad inference, junto con cualquier otra
propiedad que tendrías en un programa real.
En conjunto, esta característica puede hacer que la inferencia de TypeScript parezca un poco
como un motor de inferencia de tipos unificador, pero no lo es.
Alias de Tipo
Los alias de tipo son meros alias, al igual que type en Haskell. El
compilador intentará usar el nombre del alias dondequiera que se usó en
el código fuente, but no siempre lo logra.
tsTrytypeSize = [number, number];letx :Size = [101.1, 999.9];
El equivalente más cercano a newtype es una intersección etiquetada:
tstype 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
string, pero no al revés.
Uniones Discriminadas
El equivalente más cercano a data es una unión de tipos con propiedades
discriminantes, normalmente llamadas uniones discriminadas en TypeScript:
tstype 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 objeto. Cada variante tiene una propiedad idéntica con un
tipo unitario diferente. Esto sigue siendo un tipo unión normal; el | inicial es
una parte opcional de la sintaxis del tipo unión. Puedes discriminar los
miembros de la unión usando código JavaScript normal:
tsTrytypeShape =| {kind : "circle";radius : number }| {kind : "square";x : number }| {kind : "triangle";x : number;y : number };functionarea (s :Shape ) {if (s .kind === "circle") {returnMath .PI *s .radius *s .radius ;} else if (s .kind === "square") {returns .x *s .x ;} else {return (s .x *s .y ) / 2;}}
Nota que el tipo de retorno de area se infiere como number porque
TypeScript sabe que la función es total. Si alguna variante no está
cubierta, el tipo de retorno de area será number | undefined en su lugar.
Además, a diferencia de Haskell, las propiedades comunes aparecen en cualquier unión, por lo que puedes discriminar útilmente múltiples miembros de la unión:
tsTryfunctionheight (s :Shape ) {if (s .kind === "circle") {return 2 *s .radius ;} else {// s.kind: "square" | "triangle"returns .x ;}}
Parámetros de Tipo
Como la mayoría de los lenguajes descendientes de C, TypeScript requiere la declaración de parámetros de tipo:
tsfunction liftArray<T>(t: T): Array<T> {return [t];}
No hay requisito de mayúsculas/minúsculas, pero los parámetros de tipo son convencionalmente
letras mayúsculas únicas. Los parámetros de tipo también pueden restringirse a un
tipo, lo que se comporta un poco como las restricciones de clases de tipo (type class):
tsfunction firstish<T extends { length: number }>(t1: T, t2: T): T {return t1.length > t2.length ? t1 : t2;}
TypeScript generalmente puede inferir argumentos de tipo de una llamada basándose en el tipo de los argumentos, por lo que los argumentos de tipo generalmente no son necesarios.
Debido a que TypeScript es estructural, no necesita parámetros de tipo tanto como los sistemas nominales. Específicamente, no son necesarios para hacer una función polimórfica. Los parámetros de tipo solo deben usarse para propagar información de tipo, como restringir parámetros para que sean del mismo tipo:
ts// Mal: T no se usa para relacionar tipos de parámetrosfunction lengthBad<T extends ArrayLike<unknown>>(t: T): number {}// Bueno: Sin parámetro de tipo innecesariofunction lengthGood(t: ArrayLike<unknown>): number {}
En el primer length, T no es necesario; observa que solo se referencia
una vez, por lo que no se usa para restringir el tipo del valor
de retorno u otros parámetros.
Tipos de orden superior
TypeScript no tiene tipos de orden superior (higher kinded types), por lo que lo siguiente no es legal:
ts// No es legalfunction length<T extends ArrayLike<unknown>, U>(m: T<U>) {}
Programación sin puntos
La programación sin puntos (point-free) — uso intensivo de currificación y
composición de funciones — es posible en JavaScript, pero puede ser verbosa.
En TypeScript, la inferencia de tipos a menudo falla para programas sin puntos, por lo que
terminarás especificando parámetros de tipo en lugar de parámetros de valor. El
resultado es tan verboso que generalmente es mejor evitar la programación
sin puntos.
Sistema de Módulos
La sintaxis moderna de módulos de JavaScript es un poco como la de Haskell, excepto que
cualquier archivo con import o export es implícitamente un módulo:
tsimport { value, Type } from "npm-package";import { other, Types } from "./local-package";import * as prefix from "../lib/third-package";
También puedes importar módulos commonjs — módulos escritos usando el sistema de módulos de node.js:
tsimport f = require("single-function-package");
Puedes exportar con una lista de exportación:
tsexport { f };function f() {return g();}function g() {} // g no se exporta
O marcando cada exportación individualmente:
tsexport function f() { return g() }function g() { } // g no se exporta
El ú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 el valor predeterminado, aunque permite declaraciones de variables
con const para declarar que la referencia es
inmutable. El referente sigue siendo mutable:
jsconst a = [1, 2, 3];a.push(102); // ):a[0] = 101; // D:
TypeScript adicionalmente tiene un modificador readonly para propiedades.
tsinterface Rx {readonly x: number;}let rx: Rx = { x: 1 };rx.x = 12; // error
También incluye un tipo mapeado Readonly<T> que hace que
todas las propiedades sean readonly:
tsinterface X {x: number;}let rx: Readonly<X> = { x: 1 };rx.x = 12; // error
Y tiene un tipo específico ReadonlyArray<T> que elimina
métodos con efectos secundarios y previene la escritura en los índices del array,
así como una sintaxis especial para este tipo:
tslet a: ReadonlyArray<number> = [1, 2, 3];let b: readonly number[] = [1, 2, 3];a.push(102); // errorb[0] = 101; // error
También puedes usar una aserción const, que opera en arrays y
literales objeto:
tslet a = [1, 2, 3] as const;a.push(102); // errora[0] = 101; // error
Sin embargo, ninguna de estas opciones es la predeterminada, por lo que no se usan consistentemente en el código TypeScript.
Próximos Pasos
Este documento es una descripción general de alto nivel de la sintaxis y los tipos que usarías en el código diario. Desde aquí deberías:
- Leer el Manual completo de principio a fin
- Explorar los ejemplos del Playground