I am attempting to apply TypeScript interfaces to my Localizable
mixin. My requirements are:
- My
Localizable
mixin can be used by bothLitElement
,BaseElement
andSecondBaseElement
. - When the mixin is used, TypeScript accepts a single arg to tell the
getString
method in the mixin what are valid inputs, making the access of specific keys type safe. - TypeScript should still maintain the correct extended interface so certain things like Lit’s
@customElement
continue to work as before without errors, along with any extended methods from the class classes.
I’ve been able to get so far, but I’m still having a hard time getting it to compile correctly using TypeScript strict mode. It seems like the way I’m extending from the base class is incorrect, and as such it’s changing the actual interface of the returned super class. Both Lit doesn’t seem to like the interface, and nor do my classes that use the mixin recognize any extended methods. Ideally I’d also like to continue to accept a single argument, and instead have the desired base class be what the interface is based on.
Here’s a stripped down example I was able to put together. You’ll need to include both the lit
and @open-wc/testing
libraries.
import {LitElement} from 'lit';
import type {CSSResult} from 'lit';
import {fixture, html} from '@open-wc/testing';
import {customElement} from 'lit/decorators/custom-element.js';
/**
* Interface for methods.
*/
export interface ILocalizable<T> {
_getString(key: keyof T): string;
}
/**
* Interface for localization text.
*/
export interface ILocalizationText {
[key: string]: string;
}
/**
* Constructor for a class.
*/
type Constructor<T = object> = new (...args: unknown[]) => T;
/**
* Localizable mixin
*/
export const Localizable = <
T extends ILocalizationText,
U extends Constructor<U>,
>(
superClass: U,
): Constructor<ILocalizable<T>> => {
class LocalizableElement extends superClass {
public _getString(key: keyof T): string {
return `string, key: ${String(key)}`;
}
}
return LocalizableElement as Constructor<ILocalizable<T>> & U;
};
/**
* Base element for all components.
*/
export class BaseElement extends LitElement {
public someBaseMethod(): string {
return 'base';
}
}
/**
* Second base element which extends the first with additional functionality.
*/
export class SecondBaseElement extends BaseElement {
public someSecondBaseMethod(): string {
return 'second base';
}
}
/**
* Example component a extends from base element and wraps in mixin.
*/
@customElement('component-a')
export class ComponentA extends Localizable<{a: string}>(BaseElement) {
public static get styles(): CSSResult[] {
return [];
}
public a(): string {
return this._getString('a');
}
public b(): string {
return this.someBaseMethod();
}
}
/**
* Example component b extends from base element, no mixin.
*/
@customElement('component-b')
export class ComponentB extends BaseElement {
public static override get styles(): CSSResult[] {
return [];
}
public a(): string {
return '';
}
public b(): string {
return this.someBaseMethod();
}
}
/**
* Example component c extends from second base element and wraps in mixin.
*/
@customElement('component-c')
export class ComponentC extends Localizable<{a: string}>(SecondBaseElement) {
public static override get styles(): CSSResult[] {
return [];
}
public a(): string {
return this._getString('a');
}
public b(): string {
return this.someBaseMethod();
}
public c(): string {
return this.someSecondBaseMethod();
}
}
/**
* Example component d extends from second base element, no mixin.
*/
@customElement('component-d')
export class ComponentD extends SecondBaseElement {
public static override get styles(): CSSResult[] {
return [];
}
public a(): string {
return '';
}
public b(): string {
return this.someBaseMethod();
}
public c(): string {
return this.someSecondBaseMethod();
}
}
@customElement('component-e')
export class ComponentE extends Localizable<{a: string}>(LitElement) {
public static override get styles(): CSSResult[] {
return [];
}
public a(): string {
return this._getString('a');
}
}
/**
* Example component f extends from lit element, no mixin.
*/
@customElement('component-f')
export class ComponentF extends LitElement {
public static override get styles(): CSSResult[] {
return [];
}
public a(): string {
return '';
}
}
/**
* Load all components in a test fixture to validate their functionality.
*/
let elA: ComponentA;
let elB: ComponentB;
let elC: ComponentC;
let elD: ComponentD;
let elE: ComponentE;
let elF: ComponentF;
async function loadComponents() {
elA = await fixture(html`<component-a></component-a>`);
elA.shadowRoot?.querySelector('div');
elB = await fixture(html`<component-b></component-b>`);
elB.shadowRoot?.querySelector('div');
elC = await fixture(html`<component-c></component-c>`);
elC.shadowRoot?.querySelector('div');
elD = await fixture(html`<component-d></component-d>`);
elD.shadowRoot?.querySelector('div');
elE = await fixture(html`<component-e></component-e>`);
elE.shadowRoot?.querySelector('div');
elF = await fixture(html`<component-f></component-f>`);
elF.shadowRoot?.querySelector('div');
}
I’ve added a number of test cases as well near the bottom to validate that the interfaces are being correct assigned. I’ve based most of this on the Lit documentation which can be found here: https://lit.dev/docs/composition/mixins/#mixins-in-typescript
Any help would be appreciated here as this is throwing me through a bit of a loop.