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

TypeScript中any与unknown类型的区别与应用场景

2021-10-256.4k 阅读

any 类型

  1. 定义与特性
    • 在 TypeScript 中,any类型是一种极其灵活的类型。它表示可以是任意类型的值,就像一个“万能类型”。当我们将一个变量声明为any类型时,TypeScript 几乎不会对该变量的操作进行类型检查。这意味着我们可以对any类型的变量执行任何操作,而不会收到编译错误。
    • 例如,我们可以这样声明一个any类型的变量:
let value: any;
value = 10; // 可以赋值为数字
value = 'hello'; // 也可以赋值为字符串
value = true; // 还能赋值为布尔值
  • 不仅如此,当valueany类型时,我们可以调用它的任何方法,即使这个方法在实际类型中可能并不存在:
let value: any;
value = 'world';
console.log(value.toUpperCase()); // 正常输出 'WORLD'
value = 123;
console.log(value.toUpperCase()); // 这里在运行时会报错,因为数字没有 toUpperCase 方法,但 TypeScript 编译时不会报错
  1. 应用场景
    • 处理动态数据:在处理来自 API 响应、用户输入等动态数据时,any类型有时会很有用。例如,当我们从一个外部 API 获取数据,而该 API 的返回结构不固定或者我们还不清楚其具体结构时,可以暂时使用any类型。
async function fetchData() {
    const response = await fetch('https://example.com/api/data');
    const data: any = await response.json();
    return data;
}

async function processData() {
    const result = await fetchData();
    console.log(result.someProperty); // 这里 result 的结构不确定,使用 any 类型暂时规避类型检查
}
  • 过渡代码:当我们将 JavaScript 代码迁移到 TypeScript 代码时,可能会有大量代码还没有来得及进行详细的类型标注。此时,可以先将变量声明为any类型,使代码能够顺利编译,然后逐步对代码进行类型细化。例如,假设我们有一段 JavaScript 代码:
function add(a, b) {
    return a + b;
}
  • 迁移到 TypeScript 时,我们可以先这样写:
function add(a: any, b: any) {
    return a + b;
}
  • 然后随着对代码的理解和重构,再将any类型替换为更具体的类型,比如number类型:
function add(a: number, b: number) {
    return a + b;
}
  • 与第三方库交互:一些第三方库可能没有提供类型定义文件(.d.ts文件),在与这些库交互时,any类型可以帮助我们暂时解决类型问题。例如,假设我们使用一个老旧的 JavaScript 库legacy - library,它没有类型声明,我们在 TypeScript 项目中使用它时:
declare const legacyLibrary: any; // 使用 any 类型声明该库
const result = legacyLibrary.doSomething(); // 调用库中的方法

unknown 类型

  1. 定义与特性
    • unknown类型同样表示一个值可以是任意类型,但与any类型有本质区别。unknown类型更加安全,TypeScript 对unknown类型的值的操作会进行严格的类型检查。当一个变量是unknown类型时,我们不能直接对其进行大多数操作,除非我们先进行类型缩小。
    • 例如:
let value: unknown;
value = 42;
// console.log(value.toUpperCase()); // 这行代码会报错,TypeScript 不允许对 unknown 类型的值调用 toUpperCase 方法
  • 类型缩小可以通过类型断言、typeof检查、instanceof检查等方式来实现。比如,我们可以通过typeof来缩小unknown类型的值的范围:
let value: unknown;
value = 'hello';
if (typeof value ==='string') {
    console.log(value.toUpperCase()); // 这里通过 typeof 检查后,TypeScript 知道 value 是字符串类型,所以可以调用 toUpperCase 方法
}
  1. 应用场景
    • 函数参数:当函数接收一个可能是任意类型的值作为参数,但我们在函数内部需要谨慎处理这个值时,unknown类型非常合适。例如,假设我们有一个函数printValue,它需要处理不同类型的值,但在打印之前要确保操作是安全的:
function printValue(value: unknown) {
    if (typeof value === 'number') {
        console.log(`The number is: ${value}`);
    } else if (typeof value ==='string') {
        console.log(`The string is: ${value}`);
    } else {
        console.log('Unsupported type');
    }
}

printValue(123);
printValue('abc');
printValue({});
  • 存储动态值:与any类型处理动态数据不同,unknown类型在存储动态值时能提供更好的安全性。比如,我们有一个全局变量来存储从不同来源获取的数据,并且我们希望在使用这个变量时能有一定的类型安全保障:
let globalData: unknown;

function setGlobalData(data) {
    globalData = data;
}

function useGlobalData() {
    if (typeof globalData ==='string') {
        console.log(`The data as string: ${globalData}`);
    } else if (Array.isArray(globalData)) {
        console.log(`The data is an array with length: ${globalData.length}`);
    }
}

setGlobalData('test string');
useGlobalData();
setGlobalData([1, 2, 3]);
useGlobalData();
  • 与类型守卫结合unknown类型经常与类型守卫一起使用,类型守卫是一种运行时检查机制,它可以帮助我们在代码运行时确定一个值的类型。除了前面提到的typeofinstanceof,自定义类型守卫函数也可以和unknown类型配合使用。例如:
function isStringArray(value: unknown): value is string[] {
    return Array.isArray(value) && value.every((element) => typeof element ==='string');
}

let data: unknown = ['a', 'b', 'c'];
if (isStringArray(data)) {
    console.log(data.join(', '));
}

any 与 unknown 类型的区别本质

  1. 类型检查严格程度
    • any类型是 TypeScript 类型系统中的“宽松”类型,它几乎绕过了 TypeScript 的类型检查机制。对any类型的值进行任何操作,TypeScript 都不会在编译时报错。这在某些情况下可能会导致运行时错误,因为实际运行时该值可能并不具备我们所调用的方法或属性。
    • unknown类型是“严格”类型,TypeScript 对unknown类型的值的操作进行严格限制。只有在通过类型缩小(如类型断言、typeof检查、instanceof检查等)确定了值的具体类型后,才能对其进行相应的操作。这种严格性有助于在开发过程中尽早发现类型相关的错误,提高代码的稳定性和可靠性。
  2. 赋值兼容性
    • any类型具有很强的赋值兼容性。任何类型的值都可以赋值给any类型的变量,同时any类型的变量也可以赋值给任何其他类型的变量。例如:
let anyValue: any = 10;
let numberValue: number = anyValue; // 可以将 any 类型的值赋值给 number 类型变量
let stringValue: string = anyValue; // 也可以将 any 类型的值赋值给 string 类型变量,虽然这在运行时可能会出错
  • 对于unknown类型,赋值兼容性相对较弱。任何类型的值都可以赋值给unknown类型的变量,这与any类型相同。但是,unknown类型的变量不能直接赋值给其他具体类型的变量,除非经过类型缩小。例如:
let unknownValue: unknown = 'hello';
// let numberValue: number = unknownValue; // 这行代码会报错,不能直接将 unknown 类型的值赋值给 number 类型变量
if (typeof unknownValue === 'number') {
    let numberValue: number = unknownValue; // 通过类型缩小后可以赋值
}
  1. 类型推断
    • 当 TypeScript 进行类型推断时,如果一个表达式的类型无法确定,并且没有足够的信息来推断出更具体的类型,TypeScript 会倾向于推断为any类型,这在一些情况下可能不符合我们的预期,因为any类型会绕过类型检查。例如:
function createValue() {
    return Math.random() > 0.5? 'string' : 123;
}
let value = createValue();
// 这里 value 的类型会被推断为 any,因为返回值的类型不确定
  • 而对于unknown类型,我们可以通过显式声明来确保更安全的类型处理。例如:
function createValue(): unknown {
    return Math.random() > 0.5? 'string' : 123;
}
let value = createValue();
// 这里 value 的类型是 unknown,需要进行类型缩小后才能安全操作

如何正确选择使用 any 与 unknown 类型

  1. 追求灵活性与快速开发时
    • 如果项目处于快速开发阶段,对代码的类型安全性要求不是特别高,或者需要快速处理动态数据且对数据结构不太关心时,可以考虑使用any类型。例如,在原型开发阶段,我们更关注功能的实现,而暂时不想花费太多时间在类型标注上。
    • 以一个简单的表单数据处理为例,假设我们有一个表单,用户可以输入各种类型的数据,在原型阶段我们可以这样处理:
function processFormData(data: any) {
    console.log(data.username);
    console.log(data.age);
}

const formData = { username: 'John', age: 25 };
processFormData(formData);
  • 这里使用any类型可以快速实现功能,而不需要事先定义复杂的类型结构。但需要注意的是,在项目后续迭代中,随着对数据结构的明确,应及时将any类型替换为更具体的类型。
  1. 强调类型安全与稳定性时
    • 当项目对代码的稳定性和类型安全要求较高,尤其是在关键业务逻辑部分,应优先使用unknown类型。例如,在处理用户登录信息、财务数据等重要数据时,使用unknown类型可以确保在对数据进行操作前进行严格的类型检查。
    • 假设我们有一个处理用户登录信息的函数:
function processLoginData(data: unknown) {
    if (typeof data === 'object' && 'username' in data && 'password' in data) {
        const { username, password } = data as { username: string; password: string };
        // 进行登录验证逻辑
        console.log(`Logging in user: ${username}`);
    } else {
        console.log('Invalid login data');
    }
}

const loginData = { username: 'user1', password: 'pass1' };
processLoginData(loginData);
  • 在这个例子中,使用unknown类型可以避免在处理数据时因类型错误而导致的安全漏洞,确保只有符合特定结构的数据才能被正确处理。
  1. 与现有代码集成时
    • 如果要与没有类型声明的现有 JavaScript 代码集成,并且需要尽快让代码在 TypeScript 环境中运行,可以先使用any类型来声明相关变量或函数参数。但这只是一个过渡方案,后续应逐步添加类型声明或使用unknown类型进行更安全的处理。
    • 例如,假设我们有一个旧的 JavaScript 函数legacyFunction,它没有类型声明,我们在 TypeScript 项目中使用它:
declare function legacyFunction(): any; // 先使用 any 类型声明
const result = legacyFunction();
// 后续随着对该函数的理解,可以将其类型声明改为更具体的或者使用 unknown 类型
  • 如果我们对这个函数的返回值有一定的了解,并且希望进行更安全的处理,可以改为:
declare function legacyFunction(): unknown;
const result = legacyFunction();
if (typeof result === 'number') {
    console.log(`The result is a number: ${result}`);
}
  1. 在函数返回值类型确定时
    • 如果函数的返回值类型在不同情况下是不确定的,但我们希望调用者能够安全地处理返回值,应使用unknown类型。例如,一个根据配置返回不同类型数据的函数:
function getData(config: { type: 'number' |'string' }) {
    if (config.type === 'number') {
        return 42;
    } else {
        return 'hello';
    }
}

let data = getData({ type:'string' });
if (typeof data ==='string') {
    console.log(data.toUpperCase());
} else if (typeof data === 'number') {
    console.log(data * 2);
}
  • 这里返回值类型使用unknown,调用者可以通过类型缩小来安全地处理返回的数据。而如果使用any类型,调用者可能会在不知情的情况下进行不安全的操作。

实际项目中的最佳实践

  1. 最小化 any 类型的使用
    • 在实际项目中,应尽量减少any类型的使用范围。any类型虽然提供了灵活性,但也牺牲了类型安全。可以通过逐步细化类型来替换any类型。例如,对于一个处理 API 响应数据的函数:
// 初始使用 any 类型
async function fetchUserData(): Promise<any> {
    const response = await fetch('https://example.com/api/user');
    return response.json();
}

// 细化类型
interface User {
    id: number;
    name: string;
}
async function fetchUserData(): Promise<User> {
    const response = await fetch('https://example.com/api/user');
    return response.json() as Promise<User>;
}
  • 通过定义具体的User接口,我们将any类型替换为更明确的User类型,提高了代码的可读性和类型安全性。
  1. 合理使用 unknown 类型进行数据处理
    • 在处理来自不可信源的数据(如用户输入、第三方 API 响应等)时,unknown类型是一个很好的选择。我们可以结合类型守卫来安全地处理数据。例如,在一个处理用户上传文件的场景中:
function processUploadedFile(file: unknown) {
    if (file instanceof File) {
        const reader = new FileReader();
        reader.onload = function () {
            const text = reader.result as string;
            console.log(`File content: ${text}`);
        };
        reader.readAsText(file);
    } else {
        console.log('Invalid file');
    }
}

const uploadedFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
processUploadedFile(uploadedFile);
  • 这里使用unknown类型来表示可能是任意类型的上传文件,通过instanceof类型守卫来确定其是否为File类型,从而安全地处理文件内容。
  1. 在库开发中使用
    • 在开发 TypeScript 库时,应谨慎使用any类型。如果库的接口使用any类型,会让使用者难以获得类型提示和保证。相反,应尽量使用明确的类型定义或unknown类型,并提供清晰的类型缩小方式。例如,一个用于处理数组的库函数:
// 避免使用 any 类型
function mapArray<T, U>(array: T[], callback: (item: T) => U): U[] {
    return array.map(callback);
}

// 使用 unknown 类型的场景,假设我们有一个函数可以处理不同类型数组中的元素
function processArrayElements(array: unknown) {
    if (Array.isArray(array)) {
        for (let i = 0; i < array.length; i++) {
            if (typeof array[i] === 'number') {
                console.log(`Number at index ${i}: ${array[i]}`);
            } else if (typeof array[i] ==='string') {
                console.log(`String at index ${i}: ${array[i]}`);
            }
        }
    }
}
  • mapArray函数中,使用泛型来明确类型,而不是使用any类型。在processArrayElements函数中,使用unknown类型来处理可能是任意类型的数组,通过类型缩小来安全地处理数组元素。
  1. 结合 ESLint 规则
    • 可以结合 ESLint 规则来限制any类型的使用。例如,@typescript - eslint/no - implicit - any规则可以禁止隐式的any类型声明,@typescript - eslint/no - any规则可以完全禁止使用any类型。通过配置这些规则,可以在团队开发中强制执行更严格的类型规范。例如,在.eslintrc.json文件中:
{
    "extends": ["plugin:@typescript - eslint/recommended"],
    "rules": {
        "@typescript - eslint/no - implicit - any": "error",
        "@typescript - eslint/no - any": "error"
    }
}
  • 这样,当团队成员在代码中使用any类型时,ESLint 会报错,促使成员使用更安全的类型。

总结两种类型的特点及应用方向

  1. any 类型特点与应用方向总结
    • 特点any类型具有高度的灵活性,几乎可以绕过 TypeScript 的类型检查,允许对变量进行任何操作而不报错。它可以与任何类型相互赋值,类型推断时如果无法确定具体类型会倾向于推断为any
    • 应用方向:适用于快速开发阶段、原型设计、与无类型声明的 JavaScript 代码集成的过渡阶段,以及处理动态数据但暂时不关心其具体类型的场景。但在项目的核心业务逻辑和对稳定性要求高的部分,应避免使用any类型。
  2. unknown 类型特点与应用方向总结
    • 特点unknown类型表示任意类型,但 TypeScript 对其操作进行严格限制,需要通过类型缩小(如类型断言、typeof检查、instanceof检查等)才能安全地对其进行操作。任何类型的值可以赋值给unknown类型变量,但unknown类型变量不能直接赋值给其他具体类型变量,除非经过类型缩小。
    • 应用方向:在处理来自不可信源的数据(如用户输入、第三方 API 响应)、函数接收可能是任意类型的参数且需要安全处理的场景,以及在强调类型安全和稳定性的项目中,unknown类型是更好的选择。它能帮助开发者在运行时确定值的类型,从而避免类型相关的错误,提高代码质量。

在实际的前端开发项目中,深入理解anyunknown类型的区别,并根据具体的业务需求和项目阶段合理选择使用,对于编写高质量、类型安全的 TypeScript 代码至关重要。通过不断实践和总结经验,我们可以更好地利用这两种类型的特性,提升开发效率和代码的可维护性。