阅读视图
TypeScript 函数重载入门:让你的函数签名更精确
一、什么是函数重载?
函数重载的核心思想是:对外声明多种调用方式,对内用一个统一的实现来处理。
一个完整的函数重载包含两个主要部分:
- 重载签名:定义了函数的各种调用形式,包括参数的类型、数量和返回值的类型。这些签名没有函数体。
- 实现签名:这是函数 唯一 的实现,它包含函数体。它的签名必须能够 兼容 所有重载签名。
示例: 一个 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) 详解
一、什么是参数属性?
参数属性是一种简洁的语法,是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开发干货
前端必会:如何创建一个可随时取消的定时器
一、原生的取消方式
JavaScript 原生就提供了取消定时器的方法。setTimeout
和 setInterval
在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeout
或 clearInterval
来取消它。
// 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开发干货
TypeScript 接口入门:定义代码的契约与形态
一、什么是接口?
用于描述一个对象的结构。
// 定义一个名为 User 的接口
interface User {
id: number;
name: string;
email: string;
}
function printUserInfo(user: User) {
console.log(`ID: ${user.id}, Name: ${user.name}, Email: ${user.email}`);
}
const myUser: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
printUserInfo(myUser); // OK
const invalidUser: User = {
id: 2,
username: 'Bob', // 属性名不匹配 编译时错误
// 缺少 name,email 属性
};
二、接口的丰富特性
1. 可选属性(Optional Properties)
有时,对象的某些属性不是必需的。我们可以使用 ?
来标记它们。
interface UserProfile {
id: number;
username: string;
bio?: string; // bio 是可选的
}
const user1: UserProfile = { id: 1, username: 'Alice' }; // OK
const user2: UserProfile = { id: 2, username: 'Bob', bio: 'Developer' }; // OK
2. 只读属性(Readonly Properties)
我们可以使用 readonly
关键字来防止对象属性在创建后被修改,这对于创建不可变数据非常有用。
interface Point {
readonly x: number;
readonly y: number;
}
const p1: Point = { x: 10, y: 20 };
p1.x = 5; // Error: 无法为“x”赋值,因为它是只读属性。
3. 函数类型
接口也能用来定义函数的签名(参数类型和返回值类型)。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function (src: string, sub: string) {
let result = src.search(sub);
return result > -1;
};
console.log(mySearch('hello', 'll'));
4. 可索引类型(Indexable Types)
接口可以描述那些可以通过索引得到的类型,比如数组和对象。
interface StringArray {
[index: number]: string; // 索引是数字,值是字符串
}
let myArray: StringArray;
myArray = ['Bob', 'Fred'];
let myStr: string = myArray[0]; // OK
console.log(myStr);
interface Dictionary {
[key: string]: any; // 索引是字符串,值是任意类型
}
let user: Dictionary = {
name: '张三',
age: 18,
sex: '男',
}
console.log(user.name);
5. 类实现(Class Implementations)
接口可以被类(Class)implements
(实现),强制一个类必须遵循接口定义的契约。
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {
this.currentTime.setHours(h);
this.currentTime.setMinutes(m);
}
printTime() {
console.log(this.currentTime.toLocaleTimeString());
}
}
let clock = new Clock(12, 30);
clock.printTime(); //12:30:43
clock.setTime(new Date('2024-5-6 09:30:43'));
clock.printTime(); //09:30:43
三、接口的扩展与合并
1. 继承(Extends)
一个接口可以像类一样继承另一个接口,从而复用和扩展类型定义。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
// Square 继承了 Shape 和 PenStroke
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square: Square = {
color: 'blue',
penWidth: 5.0,
sideLength: 10,
};
2. 声明合并(Declaration Merging)
这是一个接口独有的、非常强大的特性。如果你在同一个作用域内定义了两个同名的接口,它们会自动合并成一个单一的接口。
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
// 合并后,Box 接口同时拥有 height, width, 和 scale 属性
const box: Box = { height: 5, width: 6, scale: 10 };
常用的用法
扩展第三方库的类型定义。例如,如果你想为 window
对象添加一个自定义属性,你可以这样做,而不会覆盖原有的定义:
// 在你的 .d.ts 文件中
declare global {
interface Window {
myAppConfig: object;
}
}
// 现在你可以在代码中安全地访问它
window.myAppConfig = { version: '1.0' };
总结
如果你喜欢本教程,记得点赞+收藏!关注我获取更多TypeScript开发干货