Análisis Profundo

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 import que 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, y var
  • Una declaración namespace o module que contiene un valor
  • Una declaración enum
  • Una declaración class
  • Una declaración import que 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:

ts
export var SomeVar: { a: SomeType };
export interface SomeType {
count: number;
}

Luego lo consumimos:

ts
import * 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:

ts
export var Bar: { a: Bar };
export interface Bar {
count: number;
}

Esto presenta una muy buena oportunidad para la desestructuración en el código consumidor:

ts
import { 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:

ts
interface 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:

ts
class 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:

ts
class 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:

ts
class 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:

ts
namespace 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ón namespace contiene un valor, Z)
  • Un namespace X (porque la declaración namespace contiene un tipo, Y)
  • Un tipo Y en el namespace X
  • Un tipo Z en el namespace X (la forma de la instancia de la clase)
  • Un valor Z que es una propiedad del valor X (la función constructora de la clase)

El segundo bloque crea los siguientes significados de nombre:

  • Un valor Y (de tipo number) que es una propiedad del valor X
  • Un namespace Z
  • Un valor Z que es una propiedad del valor X
  • Un tipo C en el namespace X.Z
  • Un valor C que es una propiedad del valor X.Z
  • Un tipo X

The TypeScript docs are an open source project. Help us improve these pages by sending a Pull Request

Contributors to this page:
MHMohamed Hegazy  (54)
FKFabián Karaben  (6)
1+

Last updated: 03 may 2025