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

在TypeScript中使用unknown代替any

2021-05-164.9k 阅读

1. TypeScript 中的 any 类型

在深入探讨 unknown 类型之前,我们先来回顾一下 any 类型在 TypeScript 中的表现。any 类型是 TypeScript 中一种非常宽松的类型,它允许你为变量赋予任何类型的值,并且在使用这个变量时不会进行类型检查。

1.1 any 类型的声明与赋值

let value: any;
value = 10; // 可以赋值为数字
value = 'Hello'; // 也可以赋值为字符串
value = true; // 还可以赋值为布尔值

在上述代码中,我们声明了一个 any 类型的变量 value。之后,我们可以随意地将不同类型的值赋给它,TypeScript 编译器不会抛出任何错误。

1.2 any 类型在函数参数与返回值中的应用

function logValue(value: any) {
    console.log(value);
}

logValue(42);
logValue('TypeScript');

在这个 logValue 函数中,参数 value 的类型为 any。这意味着我们可以传入任何类型的值,函数都能正常工作。同样,函数的返回值类型如果声明为 any,也不会受到类型检查的限制。

1.3 any 类型的问题

虽然 any 类型提供了极大的灵活性,但它也带来了一些严重的问题。首先,使用 any 类型会绕过 TypeScript 的类型检查机制,这可能导致在运行时出现类型错误。例如:

let num: any = '10';
let result = num + 5; // 运行时会报错,因为字符串不能直接与数字相加

在上述代码中,由于 num 被声明为 any 类型,TypeScript 编译器不会检查 num 与数字相加的操作是否合法。直到运行时,我们才会发现这个错误。

另外,过度使用 any 类型会使代码的可维护性降低。当代码库变大时,很难追踪变量的实际类型,这给后续的代码修改和调试带来了困难。

2. unknown 类型的基本概念

unknown 类型是 TypeScript 3.0 引入的一种类型,它旨在解决 any 类型带来的问题。unknown 类型表示任何类型的值,但与 any 类型不同的是,unknown 类型的值在使用之前必须进行类型检查。

2.1 unknown 类型的声明与赋值

let unknownValue: unknown;
unknownValue = 10;
unknownValue = 'Hello';
unknownValue = true;

从赋值的角度看,unknown 类型和 any 类型很相似,都可以接受任何类型的值。但关键的区别在于使用这些值时。

2.2 unknown 类型的安全性

let unknownValue: unknown = '10';
// let result = unknownValue + 5; // 这行代码会报错,因为 unknown 类型的值不能直接进行算术运算

在上述代码中,TypeScript 编译器会阻止我们对 unknownValue 进行算术运算,因为 unknown 类型的值在使用前需要进行类型检查。这有效地避免了像 any 类型那样在运行时才发现的类型错误。

3. 在函数中使用 unknown 类型

3.1 unknown 类型作为函数参数

function printValue(value: unknown) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2));
    }
}

printValue('Hello');
printValue(42);

printValue 函数中,参数 value 的类型为 unknown。通过使用 typeof 操作符进行类型检查,我们可以安全地对不同类型的值进行相应的操作。如果没有进行类型检查就直接使用 value,TypeScript 编译器会报错。

3.2 unknown 类型作为函数返回值

function getValue(): unknown {
    const randomNumber = Math.random();
    if (randomNumber > 0.5) {
        return 'Hello';
    } else {
        return 42;
    }
}

let result = getValue();
if (typeof result ==='string') {
    console.log(result.toUpperCase());
} else if (typeof result === 'number') {
    console.log(result * 2);
}

getValue 函数中,返回值类型为 unknown。调用函数后,我们需要对返回值进行类型检查,然后才能安全地使用它。

4. 类型断言与 unknown 类型

4.1 类型断言的基本概念

类型断言是一种告诉编译器某个值的类型的方式,尽管编译器可能无法自动推断出这个类型。在处理 unknown 类型的值时,类型断言可以帮助我们在进行了适当的类型检查后,更明确地使用值。

4.2 在 unknown 类型上使用类型断言

let unknownValue: unknown = 'Hello';
if (typeof unknownValue ==='string') {
    let strValue = unknownValue as string;
    console.log(strValue.length);
}

在上述代码中,我们首先通过 typeof 检查 unknownValue 是否为字符串类型。然后,使用类型断言 as stringunknownValue 断言为字符串类型,这样就可以安全地访问字符串的属性和方法。

4.3 非空断言与 unknown 类型

非空断言操作符 ! 也可以与 unknown 类型一起使用。例如:

function processValue(value: unknown) {
    let length = (value as string).length!;
    console.log(length);
}

processValue('TypeScript');

在这个例子中,我们先将 value 断言为字符串类型,然后使用非空断言操作符 ! 来确保 length 属性不为 nullundefined。不过,使用非空断言时要格外小心,因为如果断言错误,可能会导致运行时错误。

5. 类型守卫与 unknown 类型

5.1 类型守卫的定义

类型守卫是一个在运行时检查类型的表达式,它可以缩小变量的类型范围。在处理 unknown 类型的值时,类型守卫非常有用。

5.2 使用 typeof 作为类型守卫

我们前面已经看到了使用 typeof 作为类型守卫的例子:

function logValue(value: unknown) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2));
    }
}

在这个函数中,typeof value ==='string'typeof value === 'number' 就是类型守卫,它们缩小了 value 的类型范围,使得我们可以在不同的分支中安全地使用 value

5.3 用户自定义类型守卫

除了 typeof,我们还可以定义自己的类型守卫函数。例如:

function isNumber(value: unknown): value is number {
    return typeof value === 'number';
}

function processValue(value: unknown) {
    if (isNumber(value)) {
        console.log(value * 2);
    }
}

processValue(10);

在上述代码中,isNumber 函数就是一个用户自定义类型守卫。它返回一个类型谓词 value is number,告诉 TypeScript 在 if 语句的分支中,value 的类型是 number

6. unknown 类型与数组

6.1 声明 unknown 类型的数组

let unknownArray: unknown[] = [10, 'Hello', true];

这里我们声明了一个 unknown 类型的数组,数组中可以包含不同类型的元素。

6.2 遍历 unknown 类型的数组

let unknownArray: unknown[] = [10, 'Hello', true];
unknownArray.forEach((element) => {
    if (typeof element === 'number') {
        console.log(element.toFixed(2));
    } else if (typeof element ==='string') {
        console.log(element.toUpperCase());
    }
});

在遍历 unknown 类型的数组时,我们同样需要对每个元素进行类型检查,以确保安全地使用它们。

6.3 类型断言在 unknown 数组中的应用

let unknownArray: unknown[] = [10, 'Hello', true];
let numberElement = unknownArray[0] as number;
console.log(numberElement.toFixed(2));

如果我们确定数组中某个位置的元素是特定类型,可以使用类型断言来明确其类型。但要注意,这种方式需要谨慎使用,确保断言的正确性。

7. unknown 类型与对象

7.1 声明 unknown 类型的对象

let unknownObject: unknown = { name: 'John', age: 30 };

这里我们声明了一个 unknown 类型的对象,它可以是任何形状的对象。

7.2 访问 unknown 类型对象的属性

let unknownObject: unknown = { name: 'John', age: 30 };
if (typeof unknownObject === 'object' && unknownObject!== null) {
    if ('name' in unknownObject) {
        let name = (unknownObject as { name: string }).name;
        console.log(name);
    }
}

在访问 unknown 类型对象的属性时,我们首先要检查 unknownObject 是否为对象且不为 null,然后使用 in 操作符检查属性是否存在。最后,通过类型断言来安全地访问属性。

7.3 类型守卫在 unknown 对象中的应用

function isPerson(obj: unknown): obj is { name: string; age: number } {
    return (
        typeof obj === 'object' &&
        obj!== null &&
        'name' in obj &&
        'age' in obj &&
        typeof (obj as { name: string; age: number }).name ==='string' &&
        typeof (obj as { name: string; age: number }).age === 'number'
    );
}

let unknownObject: unknown = { name: 'John', age: 30 };
if (isPerson(unknownObject)) {
    console.log(unknownObject.name);
    console.log(unknownObject.age);
}

这里我们定义了一个 isPerson 类型守卫函数,用于判断 unknownObject 是否为特定形状的对象。如果通过类型守卫的检查,就可以安全地访问对象的属性。

8. 泛型与 unknown 类型

8.1 泛型函数中的 unknown 类型

function identity<T>(arg: T): T {
    return arg;
}

let result = identity<unknown>('Hello');

在泛型函数 identity 中,我们可以将类型参数 T 指定为 unknown。这样函数可以接受任何类型的值,并返回相同类型的值。

8.2 泛型类中的 unknown 类型

class Box<T> {
    private value: T;
    constructor(value: T) {
        this.value = value;
    }
    getValue(): T {
        return this.value;
    }
}

let unknownBox = new Box<unknown>(42);
let boxValue = unknownBox.getValue();
if (typeof boxValue === 'number') {
    console.log(boxValue.toFixed(2));
}

在泛型类 Box 中,我们可以将类型参数 T 设置为 unknown。在获取 Box 实例的值后,同样需要进行类型检查才能安全地使用。

9. unknown 类型与联合类型和交叉类型

9.1 unknown 类型与联合类型

let value: string | unknown;
value = 'Hello';
value = 10;

unknown 类型与其他类型组成联合类型时,由于 unknown 类型的包容性,整个联合类型的行为类似于 unknown 类型。在使用 value 时,同样需要进行类型检查。

9.2 unknown 类型与交叉类型

let value: { name: string } & unknown;
value = { name: 'John' };
if (typeof value === 'object' && value!== null && 'name' in value) {
    let name = (value as { name: string }).name;
    console.log(name);
}

unknown 类型与其他类型组成交叉类型时,在使用时也需要进行类型检查,以确保安全地访问属性。

10. 最佳实践与注意事项

10.1 尽量避免使用 any 类型

在新的 TypeScript 项目中,应优先使用 unknown 类型代替 any 类型。除非有特殊情况,如使用第三方库且无法获取其类型定义时,才考虑使用 any 类型。

10.2 正确使用类型检查和类型断言

在处理 unknown 类型的值时,要充分利用类型检查(如 typeofinstanceof 等)和类型断言,但要确保断言的正确性,避免运行时错误。

10.3 文档化类型检查逻辑

对于复杂的类型检查逻辑,尤其是涉及到用户自定义类型守卫的情况,应进行适当的文档化,以便其他开发者理解代码的行为。

通过以上对 unknown 类型的详细介绍,我们可以看到,unknown 类型在保持类型安全的前提下,为我们提供了一种灵活处理未知类型值的方式,是 TypeScript 编程中非常重要的工具。在实际开发中,合理地使用 unknown 类型可以显著提高代码的质量和可维护性。