TypeScript
TypeScript copied to clipboard
Allow top-level function calls and decorator applications to augment interfaces
Suggestion
Reduce repeated code for modules that augment global interface like HTMLElementTagNameMap
by letting functions and decorators declare how they augment the interface.
🔍 Search Terms
- HTMLElementTagNameMap
✅ Viability Checklist
My suggestion meets these guidelines:
- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x] This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
Provide a way, similar to custom type guards and assertions, for a function or decorator to declare that it augments an interface.
lib.dom.ts has a number of map interfaces that are good practice for modules to extend to add their element and event names and types to the built-in APIs that use them. Extending those interfaces is extra boilerplate though, and many authors forget to do so. It would be great if libraries that register elements or declaratively advertise events could ensure that the interfaces are extended automatically.
// This allows us to say that only the global registry augments HTMLElementTagNameMap
declare interface GlobalCustomElementRegistry extends CustomElementRegistry {
define<N extends string, C extends CustomElementConstructor>(
name: N,
constructor: C,
options?: ElementDefinitionOptions): declares global {
interface HTMLElementTagNameMap {
[N]: C;
}
}
};
}
declare var customElements: GlobalCustomElementRegistry;
Usage:
export class MyElement extends HTMLElement { /* ... */ }
// An unconditional call to the function would augment the interface
customElements.define('my-element', MyElement);
It would be great if this could work for decorators too:
export
@customElement('my-element')
class MyElement extends HTMLElement { /* ... */ }
📃 Motivating Example
See above
💻 Use Cases
Custom HTML elements, custom DOM events.
This seems extraordinarily difficult to reason about in nontrivial cases, if not impossible. Consider something like
interface Foo { }
declare function makeFooHaveBar(m: { bar?: number }): declares global {
interface Foo { bar: string }
}
declare const f: Foo;
makeFooHaveBar(f);
makeFooHaveBar(f);
Are both calls illegal, or neither? Just the second?
What if you write something like this?
interface Foo { m: string; }
declare function addPrefixedPropertiesToFoo<T, S>(arg: T, type: S): declares global {
interface Foo { [K in keyof T as `x_${K}]: S }
}
declare const f: Foo;
addPrefixedProperties(f, 0);
addPrefixedProperties(f, true);
addPrefixedProperties(f, "");
What's the type of Foo
in this program? Are these calls processed sequentially? What if several calls exist in various top-level files from which there's no graph-inferrable ordering?
@RyanCavanaugh I understand. I knew this was a super long-shot of an idea, I just threw it out there in case someone on the team actually had a realistic way of doing something here, or is sparked an idea.
I do think there are desirable answers to you questions, though.
interface Foo { }
declare function makeFooHaveBar(m: { bar?: number }): declares global {
interface Foo { bar: string }
}
declare const f: Foo;
makeFooHaveBar(f);
makeFooHaveBar(f);
Should (IMO) behave like:
interface Foo { }
declare function makeFooHaveBar(m: { bar?: number }): void;
declare const f: Foo;
makeFooHaveBar(f);
declare global {
interface Foo { bar: string }
}
makeFooHaveBar(f);
declare global {
interface Foo { bar: string }
}
Which would have an error on both calls.
This example:
interface Foo { m: string; }
declare function addPrefixedPropertiesToFoo<T, S>(arg: T, type: S): declares global {
interface Foo { [K in keyof T as `x_${K}]: S }
}
declare const f: Foo;
addPrefixedProperties(f, 0);
addPrefixedProperties(f, true);
addPrefixedProperties(f, "");
declares global {
interface Foo { [K in keyof T as `x_${K}]: S }
}
I would think would behave something like:
declare global {
interface Foo { m: string; }
}
declare const f: Foo;
declare global {
type Foo = {
[K in keyof typeof f as `x_${K}`]: 0;
}
}
// etc
which definitely has some circularity problems due to typeof f
being Foo
. Maybe that's illegal?
Anyway, again, I get this is a wild idea and probably very unrealistic. Feel free to close!