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

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

2023-03-122.8k 阅读

一、TypeScript 基础类型回顾

在深入探讨 anyunknown 类型之前,我们先来回顾一下 TypeScript 的基础类型体系。TypeScript 是 JavaScript 的超集,它为 JavaScript 增加了静态类型检查。基础类型包括布尔值(boolean)、数字(number)、字符串(string)、数组(Array)、元组(Tuple)、枚举(enum)等。

例如,定义一个布尔值变量:

let isDone: boolean = false;

定义一个数字变量:

let num: number = 42;

数组的定义方式如下:

let numbers: number[] = [1, 2, 3];

元组允许表示一个已知元素数量和类型的数组,各元素的类型不必相同:

let tuple: [string, number] = ['hello', 42];

枚举则是为一组数值赋予友好名字的方式:

enum Color {
  Red,
  Green,
  Blue
}
let myColor: Color = Color.Green;

这些基础类型为我们编写类型安全的代码提供了基本的工具。然而,在实际开发中,我们会遇到一些类型难以确定的情况,这时 anyunknown 类型就派上用场了。

二、any 类型

2.1 any 类型的定义与特点

any 类型是 TypeScript 中最灵活的类型,它允许你将变量赋值为任意类型的值,并且在使用该变量时不会进行类型检查。这意味着,你可以对 any 类型的变量执行任何操作,而 TypeScript 编译器不会报错。

例如,定义一个 any 类型的变量:

let value: any;
value = 'hello';
console.log(value.length); // 正常输出 5
value = 42;
console.log(value.toFixed(2)); // 正常输出 42.00

在上述代码中,value 变量先被赋值为字符串,然后又被赋值为数字,并且都能正常调用相应类型的方法,TypeScript 编译器没有发出任何警告。

2.2 any 类型的使用场景

  1. 动态数据获取:当从第三方 API 获取数据,且你无法提前确定数据的具体类型时,any 类型可以派上用场。例如,通过 fetch 获取 JSON 数据:
async function fetchData() {
  const response = await fetch('https://example.com/api/data');
  const data: any = await response.json();
  return data;
}
fetchData().then(data => {
  console.log(data.someProperty); // 这里 data 的类型不确定,使用 any 暂时避免类型错误
});
  1. 兼容旧代码:在将 JavaScript 项目迁移到 TypeScript 时,对于一些难以快速确定类型的旧代码部分,可以暂时使用 any 类型,以逐步完成迁移。例如:
// 旧的 JavaScript 函数,难以确定返回类型
function legacyFunction() {
  return Math.random() > 0.5? 'string' : 42;
}
// 在 TypeScript 中使用,暂时使用 any 类型
let result: any = legacyFunction();

2.3 any 类型的弊端

虽然 any 类型提供了很大的灵活性,但它也破坏了 TypeScript 的类型安全机制。过度使用 any 类型会导致代码失去类型检查的优势,增加潜在的运行时错误。

例如:

let anyValue: any = 'hello';
anyValue.toUpperCase(); // 正常
anyValue(); // 这里本意可能是调用函数,但实际上 'hello' 不是函数,运行时会报错,而 TypeScript 编译时不会提示

由于 any 类型绕过了类型检查,像上述将字符串误当作函数调用的错误在编译阶段无法被发现,只有在运行时才会暴露问题,这给代码调试带来了困难。

三、unknown 类型

3.1 unknown 类型的定义与特点

unknown 类型同样表示类型不确定的值,但与 any 类型不同的是,unknown 类型更加安全。你不能对 unknown 类型的变量直接执行操作,除非你先进行类型断言或类型缩小。

例如:

let unknownValue: unknown;
unknownValue = 'hello';
// console.log(unknownValue.length); // 报错:Object is of type 'unknown'.

在上述代码中,试图直接访问 unknownValuelength 属性会导致编译错误,因为 TypeScript 不允许对 unknown 类型进行未经检查的操作。

3.2 类型断言与类型缩小

  1. 类型断言:类型断言是告诉 TypeScript 编译器,你比它更了解某个值的类型。对于 unknown 类型,可以使用类型断言来明确其类型,从而执行相应操作。
let unknownValue: unknown;
unknownValue = 'hello';
let strLength: number = (unknownValue as string).length;
console.log(strLength); // 输出 5

在上述代码中,通过 (unknownValue as string)unknownValue 断言为字符串类型,从而可以安全地访问 length 属性。

  1. 类型缩小:类型缩小是通过条件判断来缩小 unknown 类型的范围,从而可以安全地执行操作。例如,使用 typeof 进行类型缩小:
let unknownValue: unknown;
unknownValue = 'hello';
if (typeof unknownValue ==='string') {
  console.log(unknownValue.length); // 安全访问 length 属性
}

在这个例子中,通过 typeof 判断 unknownValue 是否为字符串类型,如果是,则可以安全地访问 length 属性。

3.3 unknown 类型的使用场景

  1. 函数参数类型不确定:当编写一个函数,其参数类型可能是任意类型,但你希望在函数内部进行安全处理时,可以使用 unknown 类型。
function printValue(value: unknown) {
  if (typeof value ==='string') {
    console.log(value.toUpperCase());
  } else if (typeof value === 'number') {
    console.log(value.toFixed(2));
  }
}
printValue('hello'); // 输出 HELLO
printValue(42); // 输出 42.00

printValue 函数中,参数 valueunknown 类型,通过 typeof 进行类型缩小后,能够安全地对不同类型的值进行操作。

  1. 捕获异常的类型:在 try - catch 块中,catch 捕获到的异常类型通常是 unknown
try {
  throw new Error('Something went wrong');
} catch (error) {
  if (error instanceof Error) {
    console.log(error.message);
  }
}

在上述代码中,catch 捕获到的 error 类型是 unknown,通过 instanceof 类型缩小为 Error 类型后,可以安全地访问 message 属性。

四、any 与 unknown 类型的区别

4.1 类型安全性

any 类型是完全开放的,它绕过了 TypeScript 的类型检查,可能导致运行时错误。而 unknown 类型更加安全,它强制你在使用值之前进行类型断言或类型缩小,从而避免潜在的类型错误。

例如,以下代码使用 any 类型可能导致运行时错误:

let anyValue: any = 'hello';
anyValue(); // 运行时错误,'hello' 不是函数,TypeScript 编译时不报错

而使用 unknown 类型,编译时就会报错:

let unknownValue: unknown = 'hello';
unknownValue(); // 报错:Object is of type 'unknown'.

4.2 操作的限制

any 类型的变量可以执行任何操作,而对 unknown 类型的变量,只有在进行类型断言或类型缩小后才能执行操作。

比如,对于 any 类型:

let anyValue: any = 'hello';
anyValue.prop = 'new value'; // 虽然 'hello' 本身没有 prop 属性,但 TypeScript 编译时不报错

对于 unknown 类型:

let unknownValue: unknown = 'hello';
// unknownValue.prop = 'new value'; // 报错:Object is of type 'unknown'.

4.3 类型兼容性

any 类型与任何类型都兼容,你可以将 any 类型的值赋值给任何其他类型的变量,也可以将任何类型的值赋值给 any 类型的变量。

例如:

let anyValue: any = 'hello';
let str: string = anyValue; // 正常
let num: number = anyValue; // 虽然 'hello' 不是数字,但 TypeScript 允许
anyValue = 42;

unknown 类型只能赋值给 any 类型或 unknown 类型本身。

例如:

let unknownValue: unknown = 'hello';
let anotherUnknown: unknown = unknownValue; // 正常
let anyValue: any = unknownValue; // 正常
// let str: string = unknownValue; // 报错:Type 'unknown' is not assignable to type'string'.

五、何时选择 any 与 unknown 类型

5.1 选择 any 类型的情况

  1. 临时解决方案:在将 JavaScript 项目迁移到 TypeScript 初期,对于一些复杂且难以快速确定类型的代码片段,可以暂时使用 any 类型,以保持项目的可构建性,然后逐步优化类型。
  2. 特定的动态编程场景:当需要实现高度动态的代码逻辑,例如编写一个通用的函数,它可以接受任何类型的参数并以某种动态方式处理,且在运行时能够确保类型安全时,可以使用 any 类型。但这种情况应该谨慎使用,并且要做好充分的测试。

5.2 选择 unknown 类型的情况

  1. 函数参数与返回值类型不确定:当编写一个函数,其参数或返回值的类型在编写函数时无法确定,但你希望在函数内部或调用处进行安全的类型处理时,unknown 类型是更好的选择。通过类型断言或类型缩小,可以确保代码的类型安全性。
  2. 处理可能的异常:在 try - catch 块中捕获异常时,异常的类型通常是 unknown。使用 unknown 类型可以更安全地处理捕获到的异常,避免在处理异常时引入潜在的类型错误。

六、实际项目中的应用示例

6.1 前端数据交互场景

在前端开发中,经常会与后端 API 进行数据交互。假设我们有一个函数用于获取用户信息,但不确定后端返回的数据结构。

使用 any 类型的示例:

async function getUserInfoAny() {
  const response = await fetch('https://example.com/api/user');
  const data: any = await response.json();
  return data;
}
getUserInfoAny().then(user => {
  console.log(user.name); // 这里 user 类型不确定,可能会在运行时出错
});

使用 unknown 类型的示例:

async function getUserInfoUnknown() {
  const response = await fetch('https://example.com/api/user');
  const data: unknown = await response.json();
  return data;
}
getUserInfoUnknown().then(user => {
  if (typeof user === 'object' && user!== null && 'name' in user) {
    console.log((user as { name: string }).name);
  }
});

在上述例子中,使用 any 类型虽然简单,但存在运行时错误的风险。而使用 unknown 类型,通过类型缩小和类型断言,确保了代码的类型安全性。

6.2 插件系统开发

在开发插件系统时,插件可能以各种形式传入数据。

使用 any 类型:

function executePluginAny(plugin: any) {
  plugin.run(); // 可能会因为 plugin 实际上没有 run 方法而在运行时出错
}

使用 unknown 类型:

function executePluginUnknown(plugin: unknown) {
  if (typeof plugin === 'object' && plugin!== null && 'run' in plugin) {
    (plugin as { run: () => void }).run();
  }
}

通过对比可以看出,在插件系统开发中,unknown 类型能更好地保证代码在处理不确定类型数据时的安全性。

七、最佳实践建议

  1. 尽量避免使用 any 类型:除非是在项目迁移的过渡阶段或特定的高度动态编程场景下,应尽量避免使用 any 类型。过度使用 any 类型会削弱 TypeScript 的类型安全优势,增加代码维护成本。
  2. 优先使用 unknown 类型:当遇到类型不确定的情况时,优先考虑使用 unknown 类型。通过合理的类型断言和类型缩小,可以在保证类型安全的前提下处理不确定类型的数据。
  3. 进行充分的类型检查与测试:无论使用 any 还是 unknown 类型,都应该进行充分的类型检查和单元测试。对于 any 类型,要通过测试确保在运行时不会出现类型错误;对于 unknown 类型,要确保类型断言和类型缩小的逻辑正确。
  4. 文档化类型假设:如果在代码中使用了 anyunknown 类型,应该在代码注释中清晰地说明类型假设和处理逻辑,以便其他开发者理解和维护代码。

例如:

// 使用 any 类型,因为第三方库的类型定义不完善
// 假设这里传入的 value 是字符串类型
function someFunctionWithAny(value: any) {
  return value.toUpperCase();
}

// 使用 unknown 类型,通过类型缩小处理
function someFunctionWithUnknown(value: unknown) {
  if (typeof value ==='string') {
    return value.toUpperCase();
  }
  return null;
}

通过遵循这些最佳实践建议,可以在 TypeScript 项目中更好地使用 anyunknown 类型,提高代码的质量和可维护性。

八、总结常见误区

  1. 认为 any 类型方便就过度使用:一些开发者在遇到类型不确定的情况时,为了快速让代码通过编译,就大量使用 any 类型。但这样做会失去 TypeScript 的类型检查优势,导致潜在的运行时错误难以发现。例如在一个大型项目中,如果频繁使用 any 类型,随着代码的不断修改和扩展,很难保证类型的一致性和正确性。
  2. 不理解 unknown 类型的使用方法:部分开发者对 unknown 类型的理解不足,不知道如何通过类型断言和类型缩小来安全地使用 unknown 类型的值。例如,他们可能会尝试直接对 unknown 类型的值执行操作,而不进行任何类型检查,从而导致编译错误。
  3. 混淆 any 和 unknown 的区别:有些开发者可能会混淆 anyunknown 的类型兼容性和操作限制。比如,他们可能会错误地认为可以将 unknown 类型的值直接赋值给其他具体类型的变量,或者认为对 unknown 类型的值也可以像 any 类型那样随意执行操作。

通过了解并避免这些常见误区,可以更加准确地在 TypeScript 中使用 anyunknown 类型,编写出更加健壮和可靠的代码。

九、与其他类型的组合使用

  1. any 与数组类型的组合:当数组元素类型不确定时,可能会使用 any[] 类型。例如,从一个动态数据源获取数据,且数据元素类型多样。
let dynamicArray: any[] = [1, 'hello', true];
dynamicArray.forEach(item => {
  // 这里 item 是 any 类型,可以执行任意操作,但要注意运行时类型错误
  console.log(item);
});

然而,这种方式同样破坏了类型安全性,在实际项目中应尽量避免,除非有充分的理由和测试保障。

  1. unknown 与数组类型的组合:当数组元素类型未知时,可以使用 unknown[] 类型。在处理这种数组时,需要对每个元素进行类型缩小或断言。
let unknownArray: unknown[] = [1, 'hello', true];
unknownArray.forEach(item => {
  if (typeof item === 'number') {
    console.log(item.toFixed(2));
  } else if (typeof item ==='string') {
    console.log(item.toUpperCase());
  }
});

通过这种方式,可以在类型安全的前提下处理元素类型未知的数组。

  1. any 和 unknown 与对象类型的组合:在处理对象时,如果对象的属性类型不确定,可能会使用 { [key: string]: any } 这样的类型表示。
let anyObject: { [key: string]: any } = {
  name: 'John',
  age: 30,
  isStudent: false
};
console.log(anyObject.name);
console.log(anyObject.someUnknownProperty); // 这里可能会因为属性不存在而在运行时出错

对于 unknown 类型与对象的组合,当需要访问对象属性时,需要先进行类型缩小或断言。

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

十、未来发展趋势

随着 TypeScript 的不断发展,对 anyunknown 类型的使用和理解也可能会发生一些变化。未来,可能会有更多的工具和最佳实践来帮助开发者更好地处理类型不确定的情况,减少对 any 类型的依赖,更准确地使用 unknown 类型。

例如,TypeScript 可能会提供更智能的类型推断机制,在处理 unknown 类型时,能够更自动地进行类型缩小和断言,减少开发者手动编写类型检查代码的工作量。同时,在代码审查工具方面,也可能会对 any 类型的使用进行更严格的检查和提示,鼓励开发者使用更安全的类型处理方式。

另外,随着前端和后端开发技术的融合,在全栈开发场景下,anyunknown 类型在跨层数据传递和处理中的使用也可能会有新的规范和模式出现,以确保整个应用程序的类型安全性和稳定性。

在开源社区方面,可能会出现更多的库和工具,帮助开发者在处理不确定类型数据时遵循最佳实践,进一步提高代码质量和可维护性。总之,anyunknown 类型在 TypeScript 生态中的应用会随着技术的发展不断优化和完善。