MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript基础语法在项目中的最佳实践

2023-01-153.3k 阅读

一、TypeScript 基础类型在项目中的实践

1.1 布尔类型(boolean)

在前端项目中,布尔类型常用于表示状态。例如,在一个用户登录模块中,我们可能会有一个标志来表示用户是否已登录。

let isLoggedIn: boolean = false;
function login() {
    isLoggedIn = true;
    console.log('用户已登录');
}
function logout() {
    isLoggedIn = false;
    console.log('用户已登出');
}

这里,isLoggedIn 明确被定义为布尔类型,在后续的 loginlogout 函数中,对其赋值只能是 truefalse,这使得代码逻辑更加清晰,避免了错误赋值的可能性。

1.2 数字类型(number)

数字类型在前端开发中用途广泛,比如处理价格、数量等。在电商项目中,商品的价格和库存数量就可以用数字类型来表示。

let productPrice: number = 99.99;
let stockQuantity: number = 100;
function decreaseStock(quantity: number) {
    if (quantity <= stockQuantity) {
        stockQuantity -= quantity;
        console.log(`已减少库存 ${quantity} 件,剩余库存 ${stockQuantity} 件`);
    } else {
        console.log('库存不足');
    }
}

在上述代码中,productPricestockQuantity 被定义为 number 类型,decreaseStock 函数接受一个 number 类型的参数 quantity,这样就保证了传入参数的类型正确性,避免了因传入错误类型导致的计算错误。

1.3 字符串类型(string)

字符串类型用于处理文本信息,像用户的姓名、地址等。在一个用户注册表单处理的场景中:

let userName: string = 'John Doe';
let userAddress: string = '123 Main St';
function validateUser() {
    if (userName.length > 0 && userAddress.length > 0) {
        console.log('用户信息有效');
    } else {
        console.log('用户信息不完整');
    }
}

这里,userNameuserAddress 被定义为字符串类型,在 validateUser 函数中,基于字符串的长度来验证用户信息的完整性,明确的类型定义使得代码在处理文本信息时更加安全和可靠。

1.4 数组类型(array)

数组在前端开发中常用于存储一组相关的数据。比如,在一个图片展示组件中,我们可能会有一个图片 URL 数组。

let imageUrls: string[] = ['image1.jpg', 'image2.jpg', 'image3.jpg'];
function displayImages() {
    imageUrls.forEach((url) => {
        let img = document.createElement('img');
        img.src = url;
        document.body.appendChild(img);
    });
}

imageUrls 被定义为 string 类型的数组,displayImages 函数通过 forEach 方法遍历数组,并为每个 URL 创建一个 img 元素展示图片。明确的数组类型定义确保了数组中元素类型的一致性,防止在遍历过程中出现类型错误。

1.5 元组类型(tuple)

元组类型用于表示已知元素数量和类型的数组。在一个地图应用中,可能会用元组来表示一个点的坐标。

let point: [number, number] = [10.0, 20.0];
function movePoint(dx: number, dy: number) {
    point[0] += dx;
    point[1] += dy;
    console.log(`新的坐标: [${point[0]}, ${point[1]}]`);
}

这里,point 被定义为一个包含两个 number 类型元素的元组。movePoint 函数通过修改元组中的元素来移动点的坐标,元组类型确保了坐标数据结构的固定性和类型正确性。

1.6 枚举类型(enum)

枚举类型用于定义一组命名常量。在一个游戏项目中,我们可以用枚举来表示游戏角色的状态。

enum CharacterStatus {
    Alive,
    Dead,
    Injured
}
let playerStatus: CharacterStatus = CharacterStatus.Alive;
function updateStatus(status: CharacterStatus) {
    playerStatus = status;
    if (playerStatus === CharacterStatus.Alive) {
        console.log('角色状态:存活');
    } else if (playerStatus === CharacterStatus.Dead) {
        console.log('角色状态:死亡');
    } else {
        console.log('角色状态:受伤');
    }
}

在上述代码中,CharacterStatus 是一个枚举类型,定义了三种角色状态。playerStatus 被定义为 CharacterStatus 类型,并初始化为 AliveupdateStatus 函数根据传入的枚举值更新角色状态,并输出相应的状态信息,枚举类型使得代码中的状态表示更加直观和易于维护。

二、TypeScript 函数相关语法在项目中的实践

2.1 函数定义与类型标注

在前端项目中,函数是实现业务逻辑的关键部分。明确函数的参数类型和返回值类型可以提高代码的可维护性和稳定性。例如,在一个计算两个数之和的函数中:

function addNumbers(a: number, b: number): number {
    return a + b;
}
let result: number = addNumbers(5, 3);
console.log(`两数之和: ${result}`);

这里,addNumbers 函数明确标注了参数 abnumber 类型,返回值也是 number 类型。调用该函数时传入的参数必须是数字类型,这样就避免了因传入错误类型参数导致的运行时错误。

2.2 可选参数与默认参数

在实际开发中,有些函数的参数可能是可选的,或者有默认值。例如,在一个发送 HTTP 请求的函数中,请求头参数可能是可选的。

function sendHttpRequest(url: string, method: string = 'GET', headers?: { [key: string]: string }) {
    let request = new XMLHttpRequest();
    request.open(method, url, true);
    if (headers) {
        for (let key in headers) {
            request.setRequestHeader(key, headers[key]);
        }
    }
    request.send();
}
sendHttpRequest('https://example.com/api/data');
sendHttpRequest('https://example.com/api/data', 'POST', { 'Content-Type': 'application/json' });

sendHttpRequest 函数中,method 参数有默认值 'GET'headers 参数是可选的。这样在调用函数时,如果不需要指定请求方法或请求头,可以省略相应参数,提高了函数调用的灵活性,同时类型标注保证了参数使用的正确性。

2.3 函数重载

函数重载允许我们定义多个同名函数,但参数列表不同。在一个图形绘制库中,可能有不同参数形式的 drawShape 函数。

function drawShape(shape: 'circle', radius: number): void;
function drawShape(shape:'rectangle', width: number, height: number): void;
function drawShape(shape: string, ...args: number[]): void {
    if (shape === 'circle') {
        let radius = args[0];
        console.log(`绘制圆形,半径为 ${radius}`);
    } else if (shape ==='rectangle') {
        let width = args[0];
        let height = args[1];
        console.log(`绘制矩形,宽为 ${width},高为 ${height}`);
    }
}
drawShape('circle', 5);
drawShape('rectangle', 10, 20);

这里,通过函数重载定义了两个 drawShape 函数,根据传入的第一个参数 shape 的值和后续参数的不同,执行不同的绘制逻辑。函数重载使得代码在处理相似功能但参数不同的情况时更加清晰和易于理解。

三、TypeScript 接口(Interface)在项目中的实践

3.1 接口定义与使用

接口在 TypeScript 中用于定义对象的形状。在一个用户数据管理模块中,我们可以定义一个用户接口。

interface User {
    name: string;
    age: number;
    email: string;
}
function displayUser(user: User) {
    console.log(`姓名: ${user.name},年龄: ${user.age},邮箱: ${user.email}`);
}
let myUser: User = {
    name: 'Jane Smith',
    age: 30,
    email: 'jane@example.com'
};
displayUser(myUser);

User 接口定义了 nameageemail 三个属性及其类型。displayUser 函数接受一个符合 User 接口形状的对象作为参数,这样就确保了传入对象具有正确的属性和类型,提高了代码的健壮性。

3.2 可选属性与只读属性

接口中的属性可以是可选的或只读的。例如,在一个商品信息接口中,商品的描述可能是可选的,而商品的 ID 应该是只读的。

interface Product {
    id: number;
    name: string;
    price: number;
    description?: string;
}
let newProduct: Product = {
    id: 1,
    name: 'Widget',
    price: 19.99
};
// newProduct.id = 2; // 错误,id 是只读属性

这里,description 属性是可选的,在创建 Product 对象时可以不提供。id 属性被定义为只读,一旦对象创建后,不能再修改 id 的值,这在保护对象的关键数据不被意外修改时非常有用。

3.3 接口继承

接口可以继承其他接口,以复用和扩展接口的定义。在一个电商系统中,可能有一个基本的 Product 接口,然后有一个 DigitalProduct 接口继承自 Product 并添加了一些特有的属性。

interface Product {
    id: number;
    name: string;
    price: number;
}
interface DigitalProduct extends Product {
    downloadUrl: string;
    fileSize: number;
}
function displayDigitalProduct(product: DigitalProduct) {
    console.log(`数字商品:${product.name},价格: ${product.price},下载链接: ${product.downloadUrl},文件大小: ${product.fileSize}`);
}
let softwareProduct: DigitalProduct = {
    id: 2,
    name: 'Software App',
    price: 49.99,
    downloadUrl: 'https://example.com/download',
    fileSize: 1024
};
displayDigitalProduct(softwareProduct);

DigitalProduct 接口继承了 Product 接口的所有属性,并添加了 downloadUrlfileSize 两个属性。这样通过接口继承,我们可以在不同层次上定义和复用接口,使代码结构更加清晰和可维护。

四、TypeScript 类(Class)在项目中的实践

4.1 类的定义与实例化

在前端开发中,类常用于封装数据和行为。例如,在一个简单的计数器应用中,可以定义一个计数器类。

class Counter {
    private count: number;
    constructor() {
        this.count = 0;
    }
    increment() {
        this.count++;
    }
    decrement() {
        if (this.count > 0) {
            this.count--;
        }
    }
    getCount() {
        return this.count;
    }
}
let myCounter = new Counter();
myCounter.increment();
myCounter.increment();
console.log(`当前计数: ${myCounter.getCount()}`);

Counter 类包含一个私有属性 count,以及 incrementdecrementgetCount 三个方法。通过 new 关键字实例化 Counter 类得到 myCounter 对象,然后调用对象的方法来操作计数器的值,类的封装特性使得代码结构更加清晰,数据访问更加安全。

4.2 继承与多态

类可以通过继承来复用和扩展功能。在一个游戏角色类体系中,有一个基类 Character,然后有子类 WarriorMage

class Character {
    protected name: string;
    protected health: number;
    constructor(name: string, health: number) {
        this.name = name;
        this.health = health;
    }
    attack() {
        console.log(`${this.name} 进行普通攻击`);
    }
}
class Warrior extends Character {
    constructor(name: string, health: number) {
        super(name, health);
    }
    attack() {
        console.log(`${this.name} 进行近战攻击`);
    }
}
class Mage extends Character {
    constructor(name: string, health: number) {
        super(name, health);
    }
    attack() {
        console.log(`${this.name} 进行魔法攻击`);
    }
}
let warrior = new Warrior('Warrior1', 100);
let mage = new Mage('Mage1', 80);
warrior.attack();
mage.attack();

WarriorMage 类继承自 Character 类,并重写了 attack 方法实现了多态。通过继承,子类复用了基类的属性和方法,同时通过重写方法实现了不同的行为,使得代码在处理不同类型的游戏角色时更加灵活和可维护。

4.3 访问修饰符

TypeScript 提供了 publicprivateprotected 三种访问修饰符。public 修饰的属性和方法可以在类的外部访问,private 修饰的只能在类内部访问,protected 修饰的可以在类内部和子类中访问。

class MyClass {
    public publicProperty: string;
    private privateProperty: number;
    protected protectedProperty: boolean;
    constructor() {
        this.publicProperty = '公开属性';
        this.privateProperty = 42;
        this.protectedProperty = true;
    }
    public publicMethod() {
        console.log('这是一个公开方法');
    }
    private privateMethod() {
        console.log('这是一个私有方法');
    }
    protected protectedMethod() {
        console.log('这是一个受保护方法');
    }
}
let myObj = new MyClass();
console.log(myObj.publicProperty);
myObj.publicMethod();
// console.log(myObj.privateProperty); // 错误,无法访问私有属性
// myObj.privateMethod(); // 错误,无法访问私有方法
class SubClass extends MyClass {
    constructor() {
        super();
        console.log(this.protectedProperty);
        this.protectedMethod();
    }
}
let subObj = new SubClass();

在上述代码中,展示了不同访问修饰符的使用。通过合理使用访问修饰符,可以控制类的属性和方法的访问范围,提高代码的安全性和封装性。

五、TypeScript 类型断言与类型守卫在项目中的实践

5.1 类型断言

类型断言用于手动指定一个值的类型。在处理 DOM 元素时,可能会遇到 TypeScript 无法准确推断类型的情况。例如:

let myElement = document.getElementById('my-element');
if (myElement) {
    let inputElement = myElement as HTMLInputElement;
    inputElement.value = '设置值';
}

这里,getElementById 返回的是 HTMLElement | null 类型,通过类型断言 as HTMLInputElement,我们告诉 TypeScript myElement 实际上是一个 HTMLInputElement 类型,这样就可以访问 value 属性。类型断言在我们明确知道值的实际类型但 TypeScript 无法自动推断时非常有用,但使用时要确保断言的正确性,否则可能导致运行时错误。

5.2 类型守卫

类型守卫用于在运行时检查值的类型。在一个函数中,根据参数的类型执行不同的逻辑。

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(`字符串值: ${value}`);
    } else {
        console.log(`数值: ${value}`);
    }
}
printValue('Hello');
printValue(42);

printValue 函数中,通过 typeof 进行类型守卫,根据 value 的类型执行不同的打印逻辑。类型守卫使得代码在处理联合类型时能够更加安全和灵活地执行不同的操作。

5.3 自定义类型守卫

除了使用内置的类型守卫,我们还可以定义自己的类型守卫函数。例如,在一个验证对象是否为 User 类型的场景中:

interface User {
    name: string;
    age: number;
}
function isUser(obj: any): obj is User {
    return 'name' in obj && 'age' in obj;
}
let myObj1 = { name: 'Tom', age: 25 };
let myObj2 = { city: 'New York' };
if (isUser(myObj1)) {
    console.log(`用户 ${myObj1.name},年龄 ${myObj1.age}`);
}
if (!isUser(myObj2)) {
    console.log('这不是一个用户对象');
}

isUser 函数是一个自定义类型守卫,通过检查对象是否包含 nameage 属性来判断是否为 User 类型。在代码中使用这个自定义类型守卫,可以更加精确地处理对象类型,提高代码的可靠性。

六、TypeScript 泛型在项目中的实践

6.1 泛型函数

泛型允许我们编写可以适用于多种类型的函数。例如,在一个获取数组中第一个元素的函数中:

function getFirst<T>(array: T[]): T | undefined {
    return array.length > 0? array[0] : undefined;
}
let numbers = [1, 2, 3];
let firstNumber = getFirst(numbers);
let strings = ['a', 'b', 'c'];
let firstString = getFirst(strings);
console.log(`第一个数字: ${firstNumber}`);
console.log(`第一个字符串: ${firstString}`);

getFirst 函数是一个泛型函数,通过类型参数 T 表示数组元素的类型。这样,同一个函数可以适用于不同类型的数组,提高了代码的复用性。

6.2 泛型类

泛型也可以应用于类。例如,在一个简单的栈数据结构实现中:

class Stack<T> {
    private items: T[] = [];
    push(item: T) {
        this.items.push(item);
    }
    pop(): T | undefined {
        return this.items.pop();
    }
}
let numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
let poppedNumber = numberStack.pop();
let stringStack = new Stack<string>();
stringStack.push('Hello');
stringStack.push('World');
let poppedString = stringStack.pop();
console.log(`弹出的数字: ${poppedNumber}`);
console.log(`弹出的字符串: ${poppedString}`);

Stack 类是一个泛型类,通过类型参数 T 表示栈中元素的类型。不同的 Stack 实例可以存储不同类型的数据,泛型类使得代码在实现通用数据结构时更加灵活和可复用。

6.3 泛型约束

有时候,我们需要对泛型类型进行约束。例如,在一个获取对象某个属性值的函数中,我们希望确保传入的对象具有指定的属性。

interface HasName {
    name: string;
}
function getName<T extends HasName>(obj: T): string {
    return obj.name;
}
let person: HasName = { name: 'Alice' };
let name = getName(person);
console.log(`名字: ${name}`);

getName 函数中,通过 T extends HasName 对泛型类型 T 进行约束,确保传入的对象具有 name 属性。泛型约束使得泛型代码在保证通用性的同时,能够满足特定的类型要求,提高代码的安全性和可靠性。