如何在TypeScript中动态声明Stimulus目标类型?
用TypeScript简化Stimulus控制器的Target属性声明
问题描述
编写Stimulus控制器时,手动逐个声明has*Target、*Target、*Targets属性效率低下,尝试通过TypeScript类型编程自动生成这些属性的类型定义,但编译器报错:
Class 'default' incorrectly implements interface 'ControllerWithTarget
'
中文翻译:类“default”未正确实现接口“ControllerWithTarget”
原始代码如下:
import { Controller } from '@hotwired/stimulus'; namespace Transform { export type HasTarget<T> = { [K in keyof T as `has${Capitalize<string & K>}Target`]: boolean; }; export type Target<T> = { [K in keyof T as `${string & K}Target`]: T[K]; }; export type Targets<T> = { [K in keyof T as `${string & K}Targets`]: Array<T[K]>; }; } const targets = [ 'button', 'container', ] as const; type TargetList = typeof targets; interface Targets extends Record<TargetList[number], HTMLElement> { button: HTMLButtonElement; container: HTMLDivElement; } type ControllerWithTarget<T> = & Transform.HasTarget<T> & Transform.Target<T> & Transform.Targets<T>; // Trying to achieve something like this export default class extends Controller implements ControllerWithTarget<Targets> { static targets = [...targets]; // declare readonly hasButtonTarget: boolean; // declare readonly buttonTarget: HTMLButtonElement; // declare readonly buttonTargets: Array<HTMLButtonElement>; // declare readonly hasContainerTarget: boolean; // declare readonly containerTarget: HTMLDivElement; // declare readonly containerTargets: Array<HTMLDivElement>; disableButton(): void { if ( this.hasButtonTarget ) { this.buttonTarget.disabled = true; } } }
问题原因
- Stimulus在运行时通过
static targets数组动态注入has*Target、*Target、*Targets属性,TypeScript编译器无法识别这些动态注入的属性,因此认为类未实现接口定义的属性。 - 原始类型定义未标记这些属性为
readonly,但Stimulus注入的Target相关属性本质是只读的,类型不匹配也会导致报错。
解决方案
通过创建抽象基类整合通用类型,并添加索引签名告知编译器动态属性的存在,同时修正类型定义的只读标记:
import { Controller } from '@hotwired/stimulus'; // 定义目标元素类型的配置结构 type TargetConfig<T> = { [K in keyof T]: T[K]; }; // 自动生成Stimulus Target相关属性的类型 type StimulusTargetProperties<T> = { readonly [K in keyof T as `has${Capitalize<string & K>}Target`]: boolean; readonly [K in keyof T as `${string & K}Target`]: T[K]; readonly [K in keyof T as `${string & K}Targets`]: T[K][]; }; // 抽象基类,整合Controller和Target属性类型 abstract class TargetController<T extends TargetConfig<any>> extends Controller implements StimulusTargetProperties<T> { // 声明静态targets属性,由子类实现 static targets: readonly string[]; // 索引签名告知编译器存在动态注入的属性 readonly [key: string]: any; } // 实际控制器使用示例 const targets = ['button', 'container'] as const; // 定义每个target对应的具体元素类型 type MyTargets = { button: HTMLButtonElement; container: HTMLDivElement; }; export default class extends TargetController<MyTargets> { static targets = [...targets]; disableButton(): void { if (this.hasButtonTarget) { this.buttonTarget.disabled = true; } } }
方案说明
- 类型自动生成:通过
StimulusTargetProperties工具类型,根据传入的目标元素类型自动生成对应的has*Target、*Target、*Targets属性类型,无需手动逐个声明。 - 抽象基类:
TargetController继承自Stimulus的Controller,并实现了自动生成的属性类型,同时通过索引签名readonly [key: string]: any让编译器认可动态注入的属性。 - 类型安全:定义
MyTargets明确每个target对应的DOM元素类型,保证代码在编译阶段就能获得准确的类型提示和检查。
内容的提问来源于stack exchange,提问作者M. Wyatt
相关产品推荐
相关产品推荐

