You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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;
    }
  }
}

问题原因

  1. Stimulus在运行时通过static targets数组动态注入has*Target、*Target、*Targets属性,TypeScript编译器无法识别这些动态注入的属性,因此认为类未实现接口定义的属性。
  2. 原始类型定义未标记这些属性为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;
    }
  }
}

方案说明

  1. 类型自动生成:通过StimulusTargetProperties工具类型,根据传入的目标元素类型自动生成对应的has*Target、*Target、*Targets属性类型,无需手动逐个声明。
  2. 抽象基类:TargetController继承自Stimulus的Controller,并实现了自动生成的属性类型,同时通过索引签名readonly [key: string]: any让编译器认可动态注入的属性。
  3. 类型安全:定义MyTargets明确每个target对应的DOM元素类型,保证代码在编译阶段就能获得准确的类型提示和检查。

内容的提问来源于stack exchange,提问作者M. Wyatt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 14:15:36