Description
Wasn't sure if this belongs here or in https://github.com/angular/vscode-ng-language-service
Which @angular/* package(s) are relevant/related to the feature request?
@angular/language-service
Description
This is not a duplicate of
Instead, this asks for an interface that we can easily augment to add type definitions for elements (built-in or custom, i.e. regardless if element names have hyphens or not).
Today Custom Elements (a.k.a Web Components) are more prevalent than ever, and they can be used in many major frameworks with type checking (React, Preact, Vue, Svelte, Solid.js) and other frameworks without type checking (Angular, etc).
In this day and age, we need to be able to easily add type definitions for elements, especially custom elements, but it can be useful to be able to extend built-in element types too in case Angular definitions are not caught up with new elements landing in browsers.
Proposed solution
Allow a TypeScript interface to be augmented, where the defintion of an element name to element type can be provided.
The proposal in
could map schemas to a TypeScript type for TypeScript users specifically (and to other types in other languages), but at least TypeScript users would have a cleaner simpler way to get moving. Implementing a cross-language schema is more difficult, plus JavaScript/TypeScript is what most people are using (I don't know if other language support even exists).
Angular is the only remaining major framework to not have the ability for a specific TypeScript interface to be augmented:
- Solid.js: augment
solid-js
module'sJSX.IntrinsicElements
- Vue: augment
vue
module'sGlobalComponents
- Svelte: augment the global
svelteHTML.IntrinsicElements
or augmentsvelte/elements
module'sSvelteHTMLElements
- React: augment
react
module'sJSX.IntrinsicElements
- Preact: augment
preact
module'sJSX.IntrinsicElements
with the same definition as for react with react compat enabled - Angular: n/a
The usage could look this,
// `angular` module
export type GlobalCSSValues = 'inherit' | 'initial' | 'etc'
// augmentable interface
export interface CSSProperties {
visibility: 'hidden' | 'visible' | 'collapse' | GlobalCSSValues
display: 'none' | 'block' | 'inline' | 'etc' | GlobalCSSValues
// ... etc ...
}
// augmentable interface
export interface IntrinsicElements {
div: { /* ... */ }
button: : { /* ... */ }
// ... etc ...
}
// user code
declare module 'angular' {
// user adds additional element definitions
interface IntrinsicElements {
'my-element': {
// this covers type checking for `[foo]=` property bindings
foo?: number
// this covers type checking for `[attr.foo]=` attribute bindings
'attr.foo'?: `${number}` | number
// this covers type checking for `(some-event)=` event bindings
'on.some-event'?: SomeEvent
}
// plus generic types for Angular's special bindings can be provided out of the box (insead of the user writing them here):
& type Obj = {
// [class.foo]= bindings
[K in `class.${string}`]?: boolean
} & {
// [style.known]= bindings
[K in keyof CSSProperties as `style.${keyof CSSProperties}`]?: CSSProperties[K]
} & {
// [style.unknown]= bindings
[K in `style.${string}`]?: string
}
}
}
class SomeEvent extends Event { /* ... */ }
or similar.
Here's an example of the special style.
and class.
binding types in TypeScript playground:
The generic style.
and class.
stuff can be provided as a default with a helper from angular
, for example:
// user code
import type {IntrinsicBindings} from 'angular'
declare module 'angular' {
// user adds additional element definitions
interface IntrinsicElements {
'my-element': IntrinsicBindings & {
// this covers type checking for `[foo]=` property bindings
foo?: number
// this covers type checking for `[attr.foo]=` attribute bindings
'attr.foo'?: `${number}` | number
// this covers type checking for `(some-event)=` event bindings
'on.some-event'?: SomeEvent
}
}
}
class SomeEvent extends Event { /* ... */ }
Heck, you could even require the syntax to match, for sake of consistency with template syntax, and it is possible thanks to TypeScript template string types:
// user code
import type {IntrinsicBindings} from 'angular'
declare module 'angular' {
// user adds additional element definitions
interface IntrinsicElements {
'my-element': IntrinsicBindings & {
// this covers type checking for `[foo]=` property bindings
foo?: number
// this covers type checking for `[attr.foo]=` attribute bindings
'[attr.foo]'?: `${number}` | number
// this covers type checking for `(some-event)=` event bindings
'(some-event)'?: SomeEvent
}
}
}
class SomeEvent extends Event { /* ... */ }
// and similar for the builtin props:
export type IntrinsicBindings = {
// [class.foo]= bindings
[K in `[class.${string}]`]?: boolean
} & {
// [style.known]= bindings
[K in keyof CSSProperties as `[style.${keyof CSSProperties}]`]?: CSSProperties[K]
} & {
// [style.unknown]= bindings
[K in `[style.${string}]`]?: string
}
Library authors making custom element libraries can then make their own mapped-type helpers to map their element class properties, for example, to property, attribute, and event types, and they can even expand the class.
and style.
types if needed.
For example, given a class like this:
import {attribute, element, event} from 'some-lib' // For example, https://github.com/lume/element
export
@element('my-element')
class MyElement extends HTMLElement {
@attribute foo: "foo" | "bar" = "foo" // the initial value when no foo attribute is set (or foo attribute is removed)
@event 'onsome-event': ((event: SomeEvent) => void) | null = null
// ... implementation omitted ...
}
class SomeEvent extends Event { /* ... */ }
then a custom element author can define their own mapped type (as they already will be doing for React, Preact, Solid, Vue, Svelte, and others) to make it easy to define element types for their specific custom element library:
import type {ElementAttributesForAngular} from 'some-lib/angular-types'
declare module 'angular' {
interface IntrinsicElements {
// pick the properties to be used for Angular template types (any not listed are omitted from template type checking)
'my-element': ElementAttributesForAngular<MyEl, 'foo' | 'onsome-event'>
}
}
Alternatives considered
Implement a schema, and the tooling needed to map that to TypeScript (and other language) definitions like in #12045? Good luck! I imagine that's why #12045 has been open for 8 years since 2016.
A TypeScript interface would much easier to provide because TypeScript is the current foundation, and it would be immediately usable by a large amount of web developers.
People can also use TypeScript definitions as a source for mapping to other languages, so even having just TypeScript support would be a better starting point than a more difficult schema idea.
There are already tools like TypeScript-to-Flow converters, so starting with a TypeScript interface right now, would be valuable a lot more quickly than #12045.