Capítulo 18 de 36
When TypeScript sees two or more declarations with the same name, it doesn't always error — for specific declaration kinds (interfaces, namespaces, and namespace-with-class/function/enum combos) it merges them into one combined definition, a mechanism used to model real JavaScript patterns (functions with static properties, inner classes, patched prototypes) in a type-safe way.
class/enum create both type and value; interface/type create only type; function/variable create only value; namespace creates a namespace (and a value if it has runtime members). What merges — and how — depends on which slots the colliding declarations share.interface X { ... } declarations with the same name combine their members into one interface. Non-function members must be unique or identically typed (a real conflict is a compile error); same-named function members instead merge as overloads of one function, with later-declared interface bodies taking precedence (ordered first) over earlier ones, except that a signature whose parameter is a single string-literal type (not a union) gets bubbled to the very top regardless of declaration order — the standard trick behind DOM types like document.createElement("canvas") returning the exact HTMLCanvasElement overload instead of the generic Element fallback.let from a different block of the same merged namespace, even though both are logically "the same namespace" after merging.namespace block declared immediately after a class, function, or enum of the same name merges into it, modeling patterns JS doesn't have first-class syntax for:
namespace Album { export class AlbumLabel {} } after class Album gives Album.AlbumLabel a nested-class-like structure (the inner class must be exported to be visible from the merge, per the namespace visibility rule above).prefix/suffix-style properties onto a function value (a common vanilla-JS idiom) gets typed safely via a matching namespace block exporting those as values.namespace after an enum can add functions that operate on that enum's members, typed as if they were static enum methods.declare module "./path" { interface X { ... } }): patches an existing exported type from another module — the standard way to tell TypeScript about a runtime prototype patch (e.g. Observable.prototype.map = function() {...}) that the compiler otherwise has no static knowledge of. Limits: an augmentation can only add to declarations that already exist (no new top-level exports), and it can only target named exports, not default exports.declare global { interface Array<T> { ... } } inside a module): adds declarations to the global scope from within a module file — same rules/limits as module augmentation, just targeting the global namespace instead of another module's exports.function buildLabel(name: string): string {
return buildLabel.prefix + name;
}
namespace buildLabel {
export let prefix = "Hello, ";
}
// observable.ts
export class Observable<T> {}
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) { /* ... */ };
| Merge kind | Requires | Notes |
|---|---|---|
| Interface + interface | same name | function members become overloads; string-literal-param signatures bubble to top |
| Namespace + namespace | same name | exported members merge; unexported members stay scoped to their own block |
| Namespace + class/function/enum | namespace declared after the other | models inner classes, function statics, enum statics |
| Class + class | — | not allowed — use mixins instead |
| Module/global augmentation | declare module "x" / declare global | patches existing exports only; no new top-level declarations; named exports only |
declare module augmentation so TypeScript actually knows the new member exists — otherwise the runtime patch and the static types silently diverge.declare module "path" augmentation reuses.