Repository: angular
Issue: [language-server] TypeScript interface for element definitions (including for custom elements)
Labels: area: language-service

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

- https://github.com/angular/angular/issues/12045

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 

- https://github.com/angular/angular/issues/12045

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's `JSX.IntrinsicElements`
- **Vue**: augment `vue` module's `GlobalComponents`
- **Svelte**: augment the global `svelteHTML.IntrinsicElements` or augment `svelte/elements` module's `SvelteHTMLElements`
- **React**: augment `react` module's `JSX.IntrinsicElements`
- **Preact**: augment `preact` module's `JSX.IntrinsicElements` with the same definition as for react with react compat enabled
- **Angular**: n/a

The usage could look this,

```ts
// `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 ...
}
```
```ts
// 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:

[TypeScript playground](https://www.typescriptlang.org/play/?#code/C4TwDgpgBA4gNgewEYEM4GEDKmBqaCuEAzlALxQDkAlgHYAWEATlcBVAD6W0tVpucUIwAMYUAUGID0kqCnwBzALYQawFEjjRawJgDMUwiGO16D0LJgAKjBJEbAqxKAG8xASABuVIlSRU4LCAAXJR0VAAm4Sr8lF4+GhAxFMIIcHAoYESJHLCIqBjYeHCERO7h3mDpwZQ0CDTZAhoIwgDWSbQB9UlCojnwyGgWRSXu0lAAdJNQPROTYgC+EqCQUADySABWZC6jMgDawulEROO6CAgAuuR+NOU08qVuewDSULRQAAaHKMfjACTOIjAZj3eYfC4AfhCSHOmhQNAWUAAZDsoFAxnsgSBNKdzlcoDc7g8xGiXm8aFAWhAQAhdFALNZbEwHE4fp8sTiAVSaXSGTY7CyiGDISE+Uz7I4iC8LgsJCkaECoAgAIwhdZbciuNxnBAhZUAJgAzAAadEyJg2RhQAAU8nO4QAlLKxPLFQh9WrNtstckjicdRQ9UbTWMLQgrbb7U7Fi66m7DZ6NTs3L6fv7zoGoMDCCGZAgWs7XcAlQAWRPe9yp34BkL6OBZXNKgsxotKgCs5c1lY5EFxCEzFA0KDoFEbYYjdoQjsLceLCAAbJ3kxQe+NykRKigQAOhyOx4xLTbJ9OW7OlQB2Jc+1frzfbkKDxCtUdmpsLIA)


The generic `style.` and `class.` stuff can be provided as a default with a helper from `angular`, for example:

```ts
// 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:

```ts
// 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 { /* ... */ }
```
```ts
// 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:

```js
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:

```ts
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](https://transform.tools/typescript-to-flow) converters, so starting with a TypeScript interface right now, would be valuable a lot more quickly than #12045.