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

如何用TypeScript创建同时支持NodeList与Element的自定义选择器

问题描述

我想实现一个自定义的find选择器,替代原生的querySelector系列方法。已经给Element类型扩展了自定义方法addClass(),但querySelectorAll返回NodeList<Element>,querySelector返回Element,导致TypeScript会因为find的返回值可能是两种类型而报错。我需要这个find函数既能选择单个元素,也能选择多个元素,同时让返回值既能调用自定义方法(比如addClass),也能使用NodeList的forEach方法。

初始实现代码:

function find(selector: string) 
{
   let elements = document.body.querySelectorAll(selector);
   if (elements.length === 1) return elements[0] as Element;
   else return elements;
}

interface Element 
{
    addClass(className: string): Element;
}

Element.prototype.addClass = function (className) 
{
   this.classList.add(className);
   return this;
}

我自己尝试的解决方案是给Element接口添加forEach方法,让NodeList接口继承自定义的Element接口,这样单个Element能调用forEach,NodeList<Element>也能调用addClass,代码如下:

interface Element 
{
    addClass(className: string): Element;
    forEach(callback: Function): any;
}

interface NodeList extends Element {}

Element.prototype.forEach = function (myFunction: Function) 
{
  myFunction(this);
}

之后还在Element接口中添加了find方法:

interface Element 
{
    addClass(className: string): Element;
    forEach(callback: Function): any;
    find(selector: string): NodeList | Element;
}

Element.prototype.find = function (selector) 
{
  let elements = document.body.querySelectorAll(selector);
  if (elements.length === 1) return elements[0] as Element;
  else return elements;
}

这种方式能正常运行,但感觉不符合TypeScript的最佳实践,想知道有没有更好的实现方案?


优化实现方案

1. 避免不合理的类型继承,分别扩展接口

之前让NodeList继承Element的做法违背了类型语义(NodeList是元素集合,和单个Element是不同类型),会导致TypeScript类型系统混乱。正确的做法是分别给Element和NodeListOf<Element>扩展自定义方法:

// 扩展Element接口,添加自定义方法
interface Element {
  addClass(className: string): this;
  find(selector: string): Element | NodeListOf<Element>;
  // 如果需要单个元素支持forEach,添加规范的类型定义
  forEach(callback: (element: Element, index: number) => void): void;
}

// 扩展NodeList接口,添加自定义方法
interface NodeListOf<T extends Element> {
  addClass(className: string): this;
  find(selector: string): Element | NodeListOf<Element>;
}

2. 实现各接口的方法

实现addClass方法

// 单个元素的addClass:直接操作classList
Element.prototype.addClass = function (className: string) {
  this.classList.add(className);
  return this;
};

// 元素集合的addClass:遍历每个元素调用单个元素的addClass
NodeList.prototype.addClass = function (className: string) {
  this.forEach(el => el.addClass(className));
  return this;
};

实现find方法

注意全局find和元素实例的find要区分作用域(实例的find应该从当前元素下查询,不是document.body):

// 全局find函数:从body下查询
function find(selector: string): Element | NodeListOf<Element> {
  const elements = document.body.querySelectorAll(selector);
  return elements.length === 1 ? elements[0] : elements;
}

// 单个元素的find:从当前元素下查询子元素
Element.prototype.find = function (selector: string) {
  const elements = this.querySelectorAll(selector);
  return elements.length === 1 ? elements[0] : elements;
};

// 元素集合的find:遍历每个元素查询,合并结果
NodeList.prototype.find = function (selector: string) {
  const result: Element[] = [];
  this.forEach(el => {
    const found = el.find(selector);
    if (found instanceof NodeList) {
      result.push(...Array.from(found));
    } else {
      result.push(found);
    }
  });

  // 根据结果数量返回单个元素或NodeList
  if (result.length === 0) return document.createDocumentFragment().childNodes;
  if (result.length === 1) return result[0];
  
  // 将数组转为NodeList返回
  const fragment = document.createDocumentFragment();
  result.forEach(el => fragment.appendChild(el));
  return fragment.childNodes;
};

实现Element的forEach方法(可选)

如果需要单个元素也支持forEach,用规范的类型定义实现:

Element.prototype.forEach = function (callback: (element: Element, index: number) => void) {
  callback(this, 0);
};

3. 使用类型守卫处理返回值(可选)

如果需要明确区分返回值类型,添加类型守卫函数,让TypeScript自动推导类型:

function isElement(value: Element | NodeListOf<Element>): value is Element {
  return value instanceof Element;
}

// 使用示例
const target = find('.demo');
if (isElement(target)) {
  target.addClass('active');
  target.find('.child').addClass('highlight');
} else {
  target.forEach(el => el.addClass('active'));
  target.addClass('group-active'); // 直接调用NodeList的addClass
}

内容的提问来源于stack exchange,提问作者Doraemon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 23:27:06