javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > TypeScript 可调用接口与构造签名

TypeScript实现可调用接口与构造签名

作者:烛衔溟

本文深入探讨TypeScript中可调用接口的进阶用法,帮助开发者描述复杂函数形态,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、可调用接口(Callable Interface)

1.1 为什么需要可调用接口

之前学过函数类型表达式(如 type Fn = (a: number) => number),可以描述一个函数的形状。但当函数本身还带有属性(如 fn.defaultValue = 0),或者需要描述具有多种调用方式的重载函数时,函数类型表达式就不够用了。

可调用接口通过在接口中定义调用签名(call signature)来描述函数类型。

1.2 调用签名语法

调用签名类似箭头函数,但使用 : 而非 =>

interface StringProcessor {
    (input: string): string;  // 调用签名
}

const toUpper: StringProcessor = (input: string) => input.toUpperCase();
const toLower: StringProcessor = (input: string) => input.toLowerCase();

调用签名可以包含多个,用于描述函数重载。

interface OverloadedFunc {
    (x: string): string;
    (x: number): number;
}

const fn: OverloadedFunc = (x: any): any => {
    if (typeof x === "string") return x.toUpperCase();
    return x * 2;
};

1.3 调用签名 vs 函数类型表达式

特性函数类型表达式 (a: T) => U可调用接口 { (a: T): U }
简洁性更简洁稍长,但可扩展
重载不支持(单个签名)支持多个调用签名
额外属性不支持可以在同一接口中添加属性
泛型支持支持

如果只需要描述一个简单的函数类型,函数类型表达式足够。如果需要重载或函数对象附加属性,用可调用接口。

二、同时具有属性和调用能力的函数对象

JavaScript 中函数也是对象,可以添加属性。例如 Date.now() 是静态方法,Array.isArray 也是函数加属性。TypeScript 允许在接口中同时定义调用签名和属性签名。

interface Counter {
    (start: number): string;   // 可调用
    interval: number;          // 属性
    reset(): void;             // 方法
}

function createCounter(): Counter {
    const counter = ((start: number) => {
        return `Count: ${start}`;
    }) as Counter;
    counter.interval = 1000;
    counter.reset = () => { console.log("reset"); };
    return counter;
}

const c = createCounter();
console.log(c(5));        // "Count: 5"
console.log(c.interval);  // 1000
c.reset();                // "reset"

这种模式在库设计中常见,例如 jQuery 的 $ 既是函数又是对象。

2.1 实际案例:带默认值的加法器

interface Adder {
    (x: number): number;
    defaultValue: number;
    setDefault(value: number): void;
}

function createAdder(defaultVal: number = 0): Adder {
    const adder = (x: number) => x + adder.defaultValue;
    adder.defaultValue = defaultVal;
    adder.setDefault = (value: number) => { adder.defaultValue = value; };
    return adder;
}

const addFive = createAdder(5);
console.log(addFive(10));  // 15
addFive.setDefault(10);
console.log(addFive(10));  // 20

三、构造签名(Construct Signature)

3.1 语法

构造签名用于描述一个可以被 new 调用的构造函数类型。语法是在调用签名前加上 new 关键字。

interface PointConstructor {
    new (x: number, y: number): { x: number; y: number };
}

function createPoint(ctor: PointConstructor, x: number, y: number) {
    return new ctor(x, y);
}

class Point {
    constructor(public x: number, public y: number) {}
}

const p = createPoint(Point, 10, 20);  // { x: 10, y: 20 }

3.2 同时支持普通调用和构造调用

一个接口可以同时包含普通调用签名和构造签名,表示这个类型既可以作为普通函数调用,也可以作为构造函数使用。例如内置的 Date 类型:Date() 返回字符串,new Date() 返回日期对象。

interface DateConstructor {
    new (): Date;
    new (value: string | number): Date;
    (): string;
}

// 实际 Date 的类型定义类似这样

3.3 工厂函数中的构造签名

构造签名常用于依赖注入或工厂模式,需要将类作为参数传递并实例化。

interface Widget {
    render(): void;
}

interface WidgetConstructor {
    new (container: HTMLElement): Widget;
}

function createWidget(ctor: WidgetConstructor, container: HTMLElement): Widget {
    return new ctor(container);
}

class Button implements Widget {
    constructor(private container: HTMLElement) {}
    render() { this.container.innerHTML = "<button>Click</button>"; }
}

const btn = createWidget(Button, document.body);
btn.render();

3.4 内置构造签名示例

TypeScript 内置类型中,ArrayPromiseMap 等都有构造签名。

// Array 构造签名简化示意
interface ArrayConstructor {
    new <T>(...items: T[]): T[];
    <T>(...items: T[]): T[];
}

const arr1 = new Array(3);   // 使用构造签名
const arr2 = Array(3);       // 使用普通调用签名(效果相同)

四、可调用接口的泛型

接口的调用签名也可以使用泛型,使函数类型更加灵活。

interface Mapper {
    <T, U>(input: T[], fn: (item: T) => U): U[];
}

const map: Mapper = (arr, fn) => arr.map(fn);

const result = map([1, 2, 3], x => x.toString());  // result 类型为 string[]

也可以将泛型参数放在接口名后面,这样所有调用签名共享同组泛型参数(但通常放在调用签名上更灵活)。

interface GenericMapper<T, U> {
    (input: T[], fn: (item: T) => U): U[];
}

const map2: GenericMapper<number, string> = (arr, fn) => arr.map(fn);
// 但这样限制了只能用于 number -> string

五、常见错误与注意事项

5.1 调用签名与属性签名冲突

同一个接口中,不能有同名的调用签名和属性签名,因为调用签名本质上是一个名为 ( ) 的特殊成员,但不会与普通属性冲突。

5.2 实现可调用接口时类型不匹配

interface Fn {
    (x: number): number;
}

const fn: Fn = (x: string) => x.length;  // ❌ 参数类型不匹配

5.3 构造签名与普通函数混淆

如果接口只有构造签名,那么只有 new 调用才符合类型,普通调用会报错。

interface Ctor {
    new (x: number): number;
}

const f: Ctor = (x: number) => x;  // ❌ 不是构造签名

5.4 在对象字面量中定义可调用对象

直接在对象字面量中定义一个既有属性又可以被调用的对象,需要使用 Object.assign 或类型断言。

const fn = Object.assign(
    (x: number) => x * 2,
    { multiplier: 2 }
);

// 或者使用类型断言
const fn2 = ((x: number) => x * 2) as { (x: number): number; multiplier: number };
fn2.multiplier = 2;

六、综合示例

// 定义一个任务管理器,可调用执行任务,同时可添加任务和查看状态
interface TaskManager {
    // 可调用:执行所有任务
    (): void;
    // 属性
    tasks: string[];
    count: number;
    // 方法
    add(task: string): void;
    clear(): void;
}

function createTaskManager(): TaskManager {
    const manager = (() => {
        console.log("Executing tasks:", manager.tasks);
        manager.tasks.forEach(t => console.log(`- ${t}`));
    }) as TaskManager;
    
    manager.tasks = [];
    manager.count = 0;
    manager.add = (task: string) => {
        manager.tasks.push(task);
        manager.count = manager.tasks.length;
    };
    manager.clear = () => {
        manager.tasks = [];
        manager.count = 0;
    };
    return manager;
}

const tasks = createTaskManager();
tasks.add("Write code");
tasks.add("Test");
tasks();   // 执行任务
console.log(tasks.count);  // 2

// 构造签名示例:模型工厂
interface Model {
    save(): void;
}

interface ModelConstructor {
    new (data: Record<string, any>): Model;
}

function createModel(ctor: ModelConstructor, data: Record<string, any>): Model {
    return new ctor(data);
}

class User implements Model {
    constructor(public data: Record<string, any>) {}
    save() { console.log("Saving user", this.data); }
}

const user = createModel(User, { name: "Alice" });
user.save();

// 带构造签名和普通调用签名的混合接口示例(类似 Date)
interface DateTime {
    new (timestamp?: number): Date;
    (): string;
}

// 实际使用中,DateTime 类型需要匹配 Date 构造函数的行为
const MyDate: DateTime = Date as any;
console.log(MyDate());           // 字符串
console.log(new MyDate());       // Date 对象

七、小结

概念语法示例说明
调用签名{ (x: number): string }描述函数类型
可调用接口interface Fn { (x: number): number; }接口形式的函数类型
带属性的函数对象interface Cnt { (): void; count: number; }同时可调用和存储属性
构造签名{ new (x: number): Point; }描述构造函数类型
混合签名{ new (x: number): T; (x: number): T; }既可作为构造函数,也可作为函数调用
泛型调用签名{ <T>(arr: T[]): T; }调用签名中使用泛型

到此这篇关于TypeScript实现可调用接口与构造签名的文章就介绍到这了,更多相关TypeScript 可调用接口与构造签名内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文