普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月21日首页
昨天 — 2025年8月20日首页

TypeScript 函数重载入门:让你的函数签名更精确

作者 烛阴
2025年8月20日 11:22

一、什么是函数重载?

函数重载的核心思想是:对外声明多种调用方式,对内用一个统一的实现来处理。

一个完整的函数重载包含两个主要部分:

  1. 重载签名:定义了函数的各种调用形式,包括参数的类型、数量和返回值的类型。这些签名没有函数体。
  2. 实现签名:这是函数 唯一 的实现,它包含函数体。它的签名必须能够 兼容 所有重载签名。

示例: 一个 add 函数,既可以用于数字相加,也可以用于字符串拼接。

// 1. 重载签名 (Overload Signatures)
function add(x: number, y: number): number;
function add(x: string, y: string): string;

// 2. 实现签名 (Implementation Signature)
function add(x: any, y: any): any {
    // 3. 函数体实现
    if (typeof x === 'number' && typeof y === 'number') {
        return x + y;
    }
    if (typeof x === 'string' && typeof y === 'string') {
        return x + y;
    }
    throw new Error('Invalid arguments');
}

// 调用
const numSum = add(5, 10); // numSum 的类型被推断为 number
console.log(numSum); // 15
const strSum = add('Hello, ', 'World!'); // strSum 的类型被推断为 string
console.log(strSum); // Hello, World!

分析:

  • 外部可见性:当外部代码调用 add 函数时,TypeScript 会看到两个重载签名。它会根据你传入的参数,从上到下查找第一个匹配的签名。
  • 内部实现:实现签名 function add(x: any, y: any): any 对外部是不可见的,你不能用 (any, any) 的方式直接调用 add 函数(除非强制类型转换)。
  • 兼容性:实现签名必须涵盖所有重载签名。在这里,x: any, y: any 可以接受 (number, number)(string, string) 的情况。

二、重载的顺序

函数重载的顺序至关重要,因为 TypeScript 在解析调用时会 按顺序检查。一旦找到匹配的签名,它就会停止搜索。

  • 顺序一般是代码中从上而下的顺序。
  • 注:在有类型包含关系的情况下一般有小而大,例如:先number,再any

function move(p: Point): void; 
function move(p: any): void; 

// ... 实现 ...
function move(p: any): void {
  // ...
}

move({ x: 1, y: 2 }); // OK, p 的类型被正确推断为 Point

三、常见的几种函数重载的优化方案

1. 联合类型的应用

参数类型不同,但逻辑和返回类型相似

// 使用重载 (显得冗余)
function printId(id: number): void;
function printId(id: string): void;
function printId(id: number | string): void {
  console.log("Your ID is: " + id);
}

// 使用联合类型 (更简洁)
function printIdSimple(id: number | string): void {
  console.log("Your ID is: " + id);
}

2. 可选参数 或 默认参数

如果只是参数数量不同,可以使用 可选参数默认参数

// 使用重载
function greet(person: string): string;
function greet(person: string, greeting: string): string;
function greet(person: string, greeting?: string): string {
  return `${greeting || "Hello"}, ${person}!`;
}

// 使用可选参数 (更简洁)
function greetSimple(person: string, greeting?: string): string {
    return `${greeting || "Hello"}, ${person}!`;
}

3. 泛型

当函数的输入类型和输出类型之间存在一种模式或关联,但具体的类型是可变的,泛型 是最佳选择。

// 使用重载 (无法穷举所有类型)
function getFirstElement(arr: number[]): number | undefined;
function getFirstElement(arr: string[]): string | undefined;
function getFirstElement(arr: any[]): any | undefined {
    return arr[0];
}

// 使用泛型 (终极解决方案)
function getFirstElementGeneric<T>(arr: T[]): T | undefined {
    return arr[0];
}

const firstNum = getFirstElementGeneric<number>([1, 2, 3]); // 推断为 number
console.log(firstNum);
const firstStr = getFirstElementGeneric<string>(['a', 'b', 'c']); // 推断为 string
console.log(firstStr);

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

昨天以前首页

精简之道:TypeScript 参数属性 (Parameter Properties) 详解

作者 烛阴
2025年8月18日 22:13

一、什么是参数属性?

参数属性是一种简洁的语法,是TypeScript独特的语法糖,它允许你在构造函数的参数列表中,通过添加访问修饰符(public, private, protected)或 readonly 关键字,来一次性完成属性的声明和初始化

示例:

class User {
    constructor(public name: string, private age: number, readonly id: number) {
        // 构造函数体可以是空的,因为声明和赋值已经自动完成了!
        // TypeScript 在幕后为你做了三件事:
        // 1. 声明了一个 public 的 name 属性。
        // 2. 声明了一个 private 的 age 属性。
        // 3. 声明了一个 readonly 的 id 属性。
        // 4. 自动完成了 this.name = name, this.age = age, this.id = id。
    }

    public getAge(): number {
        return this.age; // age 是 private 的,但可以在类内部访问
    }
}

const user = new User('Alice', 30, 123);
console.log(user.name); // "Alice" (public, 可访问)
// console.log(user.age); // Error: 属性'age'是私有的,只能在类'User'中访问。
console.log(user.getAge()); // 30 (通过公共方法访问)
console.log(user.id); // 123 (readonly, 可访问但不可修改)
// user.id = 456; // Error: 无法分配到 'id' ,因为它是只读属性。


二、参数属性的规则与组合

参数属性不仅仅是 private 的专利,它可以与所有访问修饰符以及 readonly 组合使用:

  • public:成员在任何地方都可见。(如果省略修饰符,参数默认不会成为属性)。
  • private:成员只能在声明它的类的内部访问。
  • protected:成员可以在声明它的类及其子类的内部访问。
  • readonly:成员在初始化后不能被再次赋值,有助于创建不可变(immutable)对象。

你可以自由组合它们(readonly 通常与访问修饰符一起使用)。

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

前端必会:如何创建一个可随时取消的定时器

作者 烛阴
2025年8月17日 20:58

一、原生的取消方式

JavaScript 原生就提供了取消定时器的方法。setTimeoutsetInterval 在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeoutclearInterval 来取消它。

// 1. 设置一个定时器
const timerId: number = setTimeout(() => {
  console.log("这个消息可能永远不会被打印");
}, 2000);

// 2. 在它触发前取消它
clearTimeout(timerId);

常见痛点:

  • timerId 变量需要被保留在组件或模块的作用域中,状态分散。
  • 启动、暂停、取消的逻辑是割裂的,代码可读性和可维护性差。

二、封装一个可取消的定时器类

我们可以简单的封装一个 CancellableTimer 类,将定时器的状态和行为内聚在一起。后续可以扩展,把项目中的所有定时器进行统一管理。

// 定义定时器ID类型
type TimeoutId = ReturnType<typeof setTimeout>;

class CancellableTimer {
    private timerId: TimeoutId | null = null;

    constructor(private callback: () => void, private delay: number) {}

    public start(): void {
        // 防止重复启动
        if (this.timerId !== null) {
            this.cancel();
        }

        this.timerId = setTimeout(() => {
            this.callback();
            // 执行完毕后重置 timerId
            this.timerId = null;
        }, this.delay);
    }

    public cancel(): void {
        if (this.timerId !== null) {
            clearTimeout(this.timerId);
            this.timerId = null;
        }
    }
}

// 使用示例
console.log('定时器将在3秒后触发...');
const myTimer = new CancellableTimer(() => {
    console.log('定时器任务执行!');
}, 3000);

myTimer.start();

// 模拟在1秒后取消
setTimeout(() => {
    console.log('用户取消了定时器。');
    myTimer.cancel();
}, 1000);

三、实现可暂停和恢复的定时器

在很多场景下,我们需要的不仅仅是取消,还有暂停恢复

要实现这个功能,我们需要在暂停时记录剩余时间

type TimeoutId = ReturnType<typeof setTimeout>;

class AdvancedTimer {
    private timerId: TimeoutId | null = null;
    private startTime: number = 0;
    private remainingTime: number;
    private callback: () => void;
    private delay: number;


    constructor(callback: () => void, delay: number) {
        this.remainingTime = delay;
        this.callback = callback;
        this.delay = delay;
    }

    public resume(): void {
        if (this.timerId) {
            return; // 已经在运行
        }

        this.startTime = Date.now();
        this.timerId = setTimeout(() => {
            this.callback();
            // 任务完成,重置
            this.remainingTime = this.delay;
            this.timerId = null;
        }, this.remainingTime);
    }

    public pause(): void {
        if (!this.timerId) {
            return;
        }

        clearTimeout(this.timerId);
        this.timerId = null;
        // 计算并更新剩余时间
        const timePassed = Date.now() - this.startTime;
        this.remainingTime -= timePassed;
    }

    public cancel(): void {
        if (this.timerId) {
            clearTimeout(this.timerId);
        }
        this.timerId = null;
        this.remainingTime = this.delay; // 重置
    }
}

// 使用示例
console.log('定时器启动,5秒后执行...');
const advancedTimer = new AdvancedTimer(() => console.log('Done!'), 5000);
advancedTimer.resume();

setTimeout(() => {
    console.log('2秒后暂停定时器');
    advancedTimer.pause();
}, 2000);

setTimeout(() => {
    console.log('4秒后恢复定时器 , 应该还剩3秒');
    advancedTimer.resume();
}, 4000);

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

❌
❌