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

TypeScript中的字面量类型与类型守卫

2022-09-086.2k 阅读

字面量类型

在TypeScript中,字面量类型是指能够精确表示某个具体值的类型。常见的字面量类型包括字符串字面量类型、数字字面量类型和布尔字面量类型。

字符串字面量类型

字符串字面量类型允许我们指定一个字符串必须是某个特定的值。例如,假设我们有一个函数,它接收一个表示颜色的字符串参数,并且只接受“red”、“green”或“blue”这三种颜色值:

function printColor(color: 'red' | 'green' | 'blue') {
    console.log(`The color is ${color}`);
}

printColor('red');
// printColor('yellow'); // 这行会报错,因为 'yellow' 不在允许的字面量类型范围内

在上述代码中,color参数的类型被定义为'red' | 'green' | 'blue',这是一个字符串字面量联合类型。只有'red''green''blue'这三个具体的字符串值才被允许作为参数传递给printColor函数。

数字字面量类型

数字字面量类型同样允许我们指定一个数字必须是某个特定的值。例如,我们定义一个函数,它接收一个表示月份的数字,并且只接受1到12之间的数字:

function printMonth(month: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) {
    console.log(`The month is ${month}`);
}

printMonth(5);
// printMonth(13); // 这行会报错,因为13不在允许的字面量类型范围内

这里month参数的类型是由1到12的数字字面量组成的联合类型。只有这些特定的数字才能作为参数传递给printMonth函数。

布尔字面量类型

布尔字面量类型就是truefalse。例如,我们有一个函数,根据布尔值来执行不同的操作:

function handleBoolean(value: true) {
    console.log('The value is true');
}

handleBoolean(true);
// handleBoolean(false); // 这行会报错,因为参数类型必须是true

在这个例子中,handleBoolean函数只接受true作为参数,其参数类型被定义为布尔字面量类型true

字面量类型的用途

  1. 提高代码的精确性:字面量类型可以让我们在类型定义中精确指定允许的值,避免意外传入不恰当的值,从而提高代码的健壮性。例如,在上面颜色的例子中,避免了传入无效的颜色值,减少了运行时错误的可能性。
  2. 增强代码的可读性:通过使用字面量类型,代码的意图更加清晰。从类型定义中就能直接看出函数或变量允许的具体值,而不需要去猜测或者查看其他地方的注释。比如在月份的例子中,其他人阅读代码时能立刻明白printMonth函数期望的参数范围。

类型守卫

类型守卫是一种运行时检查机制,用于在代码执行过程中确定一个变量的具体类型。当我们在代码中处理联合类型时,类型守卫非常有用,因为它可以让我们在不同类型的情况下执行不同的代码逻辑。

typeof类型守卫

typeof操作符在JavaScript中用于获取变量的类型。在TypeScript中,它也可以作为一种类型守卫。例如,我们有一个联合类型string | number,并且想根据不同的类型执行不同的操作:

function printValue(value: string | number) {
    if (typeof value === 'string') {
        console.log(`The string length is ${value.length}`);
    } else {
        console.log(`The number squared is ${value * value}`);
    }
}

printValue('hello');
printValue(5);

在上述代码中,typeof value === 'string'就是一个类型守卫。当value是字符串类型时,typeof value返回'string',从而进入相应的代码块执行操作。

instanceof类型守卫

instanceof操作符用于检查一个对象是否是某个类的实例。在处理类的继承和联合类型时,instanceof可以作为类型守卫。假设我们有一个父类Animal和两个子类DogCat

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

class Dog extends Animal {
    bark() {
        console.log(`${this.name} is barking`);
    }
}

class Cat extends Animal {
    meow() {
        console.log(`${this.name} is meowing`);
    }
}

function handleAnimal(animal: Animal) {
    if (animal instanceof Dog) {
        animal.bark();
    } else if (animal instanceof Cat) {
        animal.meow();
    }
}

const dog = new Dog('Buddy');
const cat = new Cat('Whiskers');

handleAnimal(dog);
handleAnimal(cat);

handleAnimal函数中,animal instanceof Doganimal instanceof Cat是类型守卫。通过这些类型守卫,我们可以在运行时确定animal的具体类型,并调用相应的方法。

in类型守卫

in操作符可以用来检查一个对象是否包含某个属性。它也可以作为类型守卫。例如,我们有两个具有不同属性的接口AB,以及一个联合类型A | B

interface A {
    a: string;
}

interface B {
    b: number;
}

function handleAB(value: A | B) {
    if ('a' in value) {
        console.log(`Value of a is ${value.a}`);
    } else {
        console.log(`Value of b is ${value.b}`);
    }
}

const objA: A = { a: 'hello' };
const objB: B = { b: 5 };

handleAB(objA);
handleAB(objB);

handleAB函数中,'a' in value是类型守卫。如果value对象包含属性a,则说明它是A类型,否则就是B类型。

用户自定义类型守卫

除了上述内置的类型守卫,我们还可以定义自己的类型守卫函数。用户自定义类型守卫函数需要满足特定的返回值类型,即返回一个类型谓词。类型谓词的形式为parameterName is Type,其中parameterName是函数参数名,Type是要判断的类型。例如:

interface Fish {
    swim: () => void;
}

interface Bird {
    fly: () => void;
}

function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim!== undefined;
}

function handlePet(pet: Fish | Bird) {
    if (isFish(pet)) {
        pet.swim();
    } else {
        pet.fly();
    }
}

const fish: Fish = {
    swim() {
        console.log('Swimming...');
    }
};

const bird: Bird = {
    fly() {
        console.log('Flying...');
    }
};

handlePet(fish);
handlePet(bird);

在上述代码中,isFish函数就是一个用户自定义类型守卫。它通过检查pet对象是否有swim方法来判断pet是否是Fish类型。如果isFish返回true,在调用handlePet函数时,TypeScript就会知道petFish类型,从而可以安全地调用pet.swim()方法。

字面量类型与类型守卫的结合使用

当字面量类型与类型守卫结合使用时,可以实现更加复杂和精确的类型控制。例如,我们有一个函数,它接收一个联合类型'success' | 'error'和一个相应的值:

function handleResult(result: 'success' | 'error', value: string | number) {
    if (result === 'success') {
        if (typeof value === 'string') {
            console.log(`Success with string value: ${value}`);
        } else {
            console.log(`Success with number value: ${value}`);
        }
    } else {
        if (typeof value === 'string') {
            console.log(`Error with string message: ${value}`);
        } else {
            console.log(`Error with number code: ${value}`);
        }
    }
}

handleResult('success', 'operation completed');
handleResult('success', 123);
handleResult('error', 'something went wrong');
handleResult('error', 500);

在这个例子中,result === 'success'是一个基于字面量类型的类型守卫。结合typeof类型守卫,我们可以根据不同的结果类型和值的类型,执行不同的代码逻辑。

又如,我们定义一个函数,根据不同的字面量类型来调用不同的方法:

interface Circle {
    type: 'circle';
    radius: number;
    calculateArea: () => number;
}

interface Square {
    type:'square';
    sideLength: number;
    calculateArea: () => number;
}

function createShape(shapeType: 'circle' |'square', value: number): Circle | Square {
    if (shapeType === 'circle') {
        return {
            type: 'circle',
            radius: value,
            calculateArea() {
                return Math.PI * this.radius * this.radius;
            }
        };
    } else {
        return {
            type:'square',
            sideLength: value,
            calculateArea() {
                return this.sideLength * this.sideLength;
            }
        };
    }
}

function printArea(shape: Circle | Square) {
    if (shape.type === 'circle') {
        console.log(`Circle area: ${shape.calculateArea()}`);
    } else {
        console.log(`Square area: ${shape.calculateArea()}`);
    }
}

const circle = createShape('circle', 5);
const square = createShape('square', 4);

printArea(circle);
printArea(square);

在上述代码中,shape.type === 'circle'是基于字面量类型的类型守卫。通过这个类型守卫,我们可以在printArea函数中根据不同的形状类型,正确地调用相应的calculateArea方法。

字面量类型与类型守卫的注意事项

  1. 类型守卫的局限性:虽然类型守卫在运行时可以检查类型,但它们不能完全替代编译时的类型检查。例如,typeof类型守卫只能检查基本类型,对于复杂的对象类型,可能需要使用instanceof或自定义类型守卫。而且,类型守卫只是在特定的代码块内有效,一旦离开该代码块,变量的类型仍然是联合类型。
  2. 字面量类型的过多使用:过度使用字面量类型可能会导致代码变得冗长和难以维护。特别是当联合类型中包含大量的字面量值时,代码的可读性可能会下降。在这种情况下,需要权衡精确性和代码的简洁性,可能需要考虑使用更抽象的类型定义。
  3. 类型守卫的性能:虽然在大多数情况下,类型守卫对性能的影响可以忽略不计,但在性能敏感的代码中,过多的类型守卫检查可能会有一定的开销。尤其是在循环中频繁使用类型守卫时,需要注意性能问题。

字面量类型与类型守卫在实际项目中的应用

  1. 表单验证:在前端开发中,表单验证是一个常见的需求。可以使用字面量类型来定义表单字段的合法值,例如性别字段只能是“male”或“female”。然后通过类型守卫来检查用户输入的值是否合法。例如:
function validateGender(gender: string) {
    const validGenders: 'male' | 'female' = gender as'male' | 'female';
    if (validGenders ==='male' || validGenders === 'female') {
        return true;
    }
    return false;
}

const userGender = 'female';
if (validateGender(userGender)) {
    console.log('Gender is valid');
} else {
    console.log('Invalid gender');
}
  1. 状态管理:在使用状态管理库(如Redux)时,字面量类型和类型守卫可以帮助我们更好地管理状态。例如,定义一个状态的可能值,如'loading' | 'loaded' | 'error',然后在处理状态的函数中使用类型守卫来执行不同的逻辑。
interface DataState {
    status: 'loading' | 'loaded' | 'error';
    data: any;
}

function handleData(state: DataState) {
    if (state.status === 'loading') {
        console.log('Data is loading...');
    } else if (state.status === 'loaded') {
        console.log('Data loaded:', state.data);
    } else {
        console.log('Error occurred while loading data');
    }
}

const loadingState: DataState = { status: 'loading', data: null };
const loadedState: DataState = { status: 'loaded', data: { key: 'value' } };
const errorState: DataState = { status: 'error', data: null };

handleData(loadingState);
handleData(loadedState);
handleData(errorState);
  1. 路由导航:在单页应用(SPA)的路由导航中,我们可以使用字面量类型来定义不同的路由路径,然后通过类型守卫来处理不同路径的逻辑。例如:
type Route = 'home' | 'about' | 'contact';

function navigateTo(route: Route) {
    if (route === 'home') {
        console.log('Navigating to home page');
    } else if (route === 'about') {
        console.log('Navigating to about page');
    } else {
        console.log('Navigating to contact page');
    }
}

navigateTo('home');
navigateTo('about');
navigateTo('contact');

总结字面量类型与类型守卫的重要性

字面量类型和类型守卫是TypeScript中非常强大的功能,它们可以让我们在代码中实现更加精确的类型控制。字面量类型能够明确指定变量或参数允许的具体值,提高代码的精确性和可读性;类型守卫则在运行时对联合类型进行检查,根据不同的类型执行不同的逻辑,增强了代码的灵活性和健壮性。在实际项目开发中,合理运用字面量类型和类型守卫可以有效地减少错误,提高代码的可维护性和可扩展性。无论是小型项目还是大型项目,这两个特性都能为前端开发带来显著的价值。