如何用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

