Teoría de los Archivos de Declaración: Un Análisis Profundo
Estructurar módulos para dar la forma exacta de API que deseas puede ser complicado.
Por ejemplo, podríamos querer un módulo que pueda invocarse con o sin new para producir diferentes tipos,
tenga una variedad de tipos con nombre expuestos en una jerarquía,
y tenga algunas propiedades en el objeto del módulo también.
Al leer esta guía, tendrás las herramientas para escribir archivos de declaración complejos que expongan una superficie de API amigable. Esta guía se centra en las bibliotecas de módulos (o UMD) porque las opciones aquí son más variadas.
Conceptos Clave
Puedes comprender completamente cómo hacer cualquier forma de declaración entendiendo algunos conceptos clave de cómo funciona TypeScript.
Tipos
Si estás leyendo esta guía, probablemente ya sepas aproximadamente qué es un tipo en TypeScript. Sin embargo, para ser más explícito, un tipo se introduce con:
- Una declaración de alias de tipo (
type sn = number | string;) - Una declaración de interfaz (
interface I { x: number[]; }) - Una declaración de clase (
class C { }) - Una declaración de enum (
enum E { A, B, C }) - Una declaración
importque se refiere a un tipo
Cada una de estas formas de declaración crea un nuevo nombre de tipo.
Valores
Al igual que con los tipos, probablemente ya entiendas qué es un valor.
Los valores son nombres en tiempo de ejecución que podemos referenciar en expresiones.
Por ejemplo, let x = 5; crea un valor llamado x.
Nuevamente, siendo explícitos, las siguientes cosas crean valores:
- Declaraciones
let,const, yvar - Una declaración
namespaceomoduleque contiene un valor - Una declaración
enum - Una declaración
class - Una declaración
importque se refiere a un valor - Una declaración
function
Namespaces
Los tipos pueden existir en namespaces.
Por ejemplo, si tenemos la declaración let x: A.B.C,
decimos que el tipo C proviene del namespace A.B.
Esta distinción es sutil e importante — aquí, A.B no es necesariamente un tipo o un valor.
Combinaciones Simples: Un nombre, múltiples significados
Dado un nombre A, podríamos encontrar hasta tres significados diferentes para A: un tipo, un valor o un namespace.
Cómo se interpreta el nombre depende del contexto en el que se usa.
Por ejemplo, en la declaración let m: A.A = A;,
A se usa primero como un namespace, luego como un nombre de tipo, y luego como un valor.
¡Estos significados podrían terminar refiriéndose a declaraciones completamente diferentes!
Esto puede parecer confuso, pero en realidad es muy conveniente siempre que no sobrecarguemos las cosas excesivamente. Veamos algunos aspectos útiles de este comportamiento de combinación.
Combinaciones Incorporadas
Los lectores astutos notarán que, por ejemplo, class apareció tanto en la lista de tipo como en la de valor.
La declaración class C { } crea dos cosas:
un tipo C que se refiere a la forma de la instancia de la clase,
y un valor C que se refiere a la función constructora de la clase.
Las declaraciones de Enum se comportan de manera similar.
Combinaciones del Usuario
Supongamos que escribimos un archivo de módulo foo.d.ts:
tsexport var SomeVar: { a: SomeType };export interface SomeType {count: number;}
Luego lo consumimos:
tsimport * as foo from "./foo";let x: foo.SomeType = foo.SomeVar.a;console.log(x.count);
Esto funciona bastante bien, pero podríamos imaginar que SomeType y SomeVar estaban muy relacionados
tanto que te gustaría que tuvieran el mismo nombre.
Podemos usar la combinación para presentar estos dos objetos diferentes (el valor y el tipo) bajo el mismo nombre Bar:
tsexport var Bar: { a: Bar };export interface Bar {count: number;}
Esto presenta una muy buena oportunidad para la desestructuración en el código consumidor:
tsimport { Bar } from "./foo";let x: Bar = Bar.a;console.log(x.count);
Nuevamente, hemos usado Bar como tipo y como valor aquí.
Ten en cuenta que no tuvimos que declarar el valor Bar como del tipo Bar — son independientes.
Combinaciones Avanzadas
Algunos tipos de declaraciones se pueden combinar a través de múltiples declaraciones.
Por ejemplo, class C { } e interface C { } pueden coexistir y ambas contribuir propiedades a los tipos C.
Esto es legal siempre que no cree un conflicto.
Una regla general es que los valores siempre entran en conflicto con otros valores del mismo nombre a menos que se declaren como namespaces,
los tipos entrarán en conflicto si se declaran con una declaración de alias de tipo (type s = string),
y los namespaces nunca entran en conflicto.
Veamos cómo se puede usar esto.
Añadiendo usando una interface
Podemos agregar miembros adicionales a una interface con otra declaración de interface:
tsinterface Foo {x: number;}// ... en otro lugar ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
Esto también funciona con clases:
tsclass Foo {x: number;}// ... en otro lugar ...interface Foo {y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
Ten en cuenta que no podemos agregar a alias de tipo (type s = string;) usando una interfaz.
Añadiendo usando un namespace
Una declaración namespace se puede usar para agregar nuevos tipos, valores y namespaces de cualquier manera que no cree un conflicto.
Por ejemplo, podemos agregar un miembro estático a una clase:
tsclass C {}// ... en otro lugar ...namespace C {export let x: number;}let y = C.x; // OK
Ten en cuenta que en este ejemplo, agregamos un valor al lado estático de C (su función constructora).
Esto se debe a que agregamos un valor, y el contenedor de todos los valores es otro valor
(los tipos están contenidos por namespaces, y los namespaces están contenidos por otros namespaces).
Pudimos también agregar un tipo con espacio de nombres a una clase:
tsclass C {}// ... en otro lugar ...namespace C {export interface D {}}let y: C.D; // OK
En este ejemplo, no había un namespace C hasta que escribimos la declaración namespace para él.
El significado C como un namespace no entra en conflicto con los significados de valor o tipo de C creados por la clase.
Finalmente, podríamos realizar muchas fusiones diferentes usando declaraciones namespace.
Este no es un ejemplo particularmente realista, pero muestra todo tipo de comportamiento interesante:
tsnamespace X {export interface Y {}export class Z {}}// ... en otro lugar ...namespace X {export var Y: number;export namespace Z {export class C {}}}type X = string;
En este ejemplo, el primer bloque crea los siguientes significados de nombre:
- Un valor
X(porque la declaraciónnamespacecontiene un valor,Z) - Un namespace
X(porque la declaraciónnamespacecontiene un tipo,Y) - Un tipo
Yen el namespaceX - Un tipo
Zen el namespaceX(la forma de la instancia de la clase) - Un valor
Zque es una propiedad del valorX(la función constructora de la clase)
El segundo bloque crea los siguientes significados de nombre:
- Un valor
Y(de tiponumber) que es una propiedad del valorX - Un namespace
Z - Un valor
Zque es una propiedad del valorX - Un tipo
Cen el namespaceX.Z - Un valor
Cque es una propiedad del valorX.Z - Un tipo
X