TypeScript中any与unknown类型的区别与应用场景
2021-10-256.4k 阅读
any 类型
- 定义与特性
- 在 TypeScript 中,
any
类型是一种极其灵活的类型。它表示可以是任意类型的值,就像一个“万能类型”。当我们将一个变量声明为any
类型时,TypeScript 几乎不会对该变量的操作进行类型检查。这意味着我们可以对any
类型的变量执行任何操作,而不会收到编译错误。 - 例如,我们可以这样声明一个
any
类型的变量:
- 在 TypeScript 中,
let value: any;
value = 10; // 可以赋值为数字
value = 'hello'; // 也可以赋值为字符串
value = true; // 还能赋值为布尔值
- 不仅如此,当
value
是any
类型时,我们可以调用它的任何方法,即使这个方法在实际类型中可能并不存在:
let value: any;
value = 'world';
console.log(value.toUpperCase()); // 正常输出 'WORLD'
value = 123;
console.log(value.toUpperCase()); // 这里在运行时会报错,因为数字没有 toUpperCase 方法,但 TypeScript 编译时不会报错
- 应用场景
- 处理动态数据:在处理来自 API 响应、用户输入等动态数据时,
any
类型有时会很有用。例如,当我们从一个外部 API 获取数据,而该 API 的返回结构不固定或者我们还不清楚其具体结构时,可以暂时使用any
类型。
- 处理动态数据:在处理来自 API 响应、用户输入等动态数据时,
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 类型
- 定义与特性
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 方法
}
- 应用场景
- 函数参数:当函数接收一个可能是任意类型的值作为参数,但我们在函数内部需要谨慎处理这个值时,
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
类型经常与类型守卫一起使用,类型守卫是一种运行时检查机制,它可以帮助我们在代码运行时确定一个值的类型。除了前面提到的typeof
和instanceof
,自定义类型守卫函数也可以和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 类型的区别本质
- 类型检查严格程度
any
类型是 TypeScript 类型系统中的“宽松”类型,它几乎绕过了 TypeScript 的类型检查机制。对any
类型的值进行任何操作,TypeScript 都不会在编译时报错。这在某些情况下可能会导致运行时错误,因为实际运行时该值可能并不具备我们所调用的方法或属性。- 而
unknown
类型是“严格”类型,TypeScript 对unknown
类型的值的操作进行严格限制。只有在通过类型缩小(如类型断言、typeof
检查、instanceof
检查等)确定了值的具体类型后,才能对其进行相应的操作。这种严格性有助于在开发过程中尽早发现类型相关的错误,提高代码的稳定性和可靠性。
- 赋值兼容性
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; // 通过类型缩小后可以赋值
}
- 类型推断
- 当 TypeScript 进行类型推断时,如果一个表达式的类型无法确定,并且没有足够的信息来推断出更具体的类型,TypeScript 会倾向于推断为
any
类型,这在一些情况下可能不符合我们的预期,因为any
类型会绕过类型检查。例如:
- 当 TypeScript 进行类型推断时,如果一个表达式的类型无法确定,并且没有足够的信息来推断出更具体的类型,TypeScript 会倾向于推断为
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 类型
- 追求灵活性与快速开发时
- 如果项目处于快速开发阶段,对代码的类型安全性要求不是特别高,或者需要快速处理动态数据且对数据结构不太关心时,可以考虑使用
any
类型。例如,在原型开发阶段,我们更关注功能的实现,而暂时不想花费太多时间在类型标注上。 - 以一个简单的表单数据处理为例,假设我们有一个表单,用户可以输入各种类型的数据,在原型阶段我们可以这样处理:
- 如果项目处于快速开发阶段,对代码的类型安全性要求不是特别高,或者需要快速处理动态数据且对数据结构不太关心时,可以考虑使用
function processFormData(data: any) {
console.log(data.username);
console.log(data.age);
}
const formData = { username: 'John', age: 25 };
processFormData(formData);
- 这里使用
any
类型可以快速实现功能,而不需要事先定义复杂的类型结构。但需要注意的是,在项目后续迭代中,随着对数据结构的明确,应及时将any
类型替换为更具体的类型。
- 强调类型安全与稳定性时
- 当项目对代码的稳定性和类型安全要求较高,尤其是在关键业务逻辑部分,应优先使用
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
类型可以避免在处理数据时因类型错误而导致的安全漏洞,确保只有符合特定结构的数据才能被正确处理。
- 与现有代码集成时
- 如果要与没有类型声明的现有 JavaScript 代码集成,并且需要尽快让代码在 TypeScript 环境中运行,可以先使用
any
类型来声明相关变量或函数参数。但这只是一个过渡方案,后续应逐步添加类型声明或使用unknown
类型进行更安全的处理。 - 例如,假设我们有一个旧的 JavaScript 函数
legacyFunction
,它没有类型声明,我们在 TypeScript 项目中使用它:
- 如果要与没有类型声明的现有 JavaScript 代码集成,并且需要尽快让代码在 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}`);
}
- 在函数返回值类型确定时
- 如果函数的返回值类型在不同情况下是不确定的,但我们希望调用者能够安全地处理返回值,应使用
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
类型,调用者可能会在不知情的情况下进行不安全的操作。
实际项目中的最佳实践
- 最小化 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
类型,提高了代码的可读性和类型安全性。
- 合理使用 unknown 类型进行数据处理
- 在处理来自不可信源的数据(如用户输入、第三方 API 响应等)时,
unknown
类型是一个很好的选择。我们可以结合类型守卫来安全地处理数据。例如,在一个处理用户上传文件的场景中:
- 在处理来自不可信源的数据(如用户输入、第三方 API 响应等)时,
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
类型,从而安全地处理文件内容。
- 在库开发中使用
- 在开发 TypeScript 库时,应谨慎使用
any
类型。如果库的接口使用any
类型,会让使用者难以获得类型提示和保证。相反,应尽量使用明确的类型定义或unknown
类型,并提供清晰的类型缩小方式。例如,一个用于处理数组的库函数:
- 在开发 TypeScript 库时,应谨慎使用
// 避免使用 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
类型来处理可能是任意类型的数组,通过类型缩小来安全地处理数组元素。
- 结合 ESLint 规则
- 可以结合 ESLint 规则来限制
any
类型的使用。例如,@typescript - eslint/no - implicit - any
规则可以禁止隐式的any
类型声明,@typescript - eslint/no - any
规则可以完全禁止使用any
类型。通过配置这些规则,可以在团队开发中强制执行更严格的类型规范。例如,在.eslintrc.json
文件中:
- 可以结合 ESLint 规则来限制
{
"extends": ["plugin:@typescript - eslint/recommended"],
"rules": {
"@typescript - eslint/no - implicit - any": "error",
"@typescript - eslint/no - any": "error"
}
}
- 这样,当团队成员在代码中使用
any
类型时,ESLint 会报错,促使成员使用更安全的类型。
总结两种类型的特点及应用方向
- any 类型特点与应用方向总结
- 特点:
any
类型具有高度的灵活性,几乎可以绕过 TypeScript 的类型检查,允许对变量进行任何操作而不报错。它可以与任何类型相互赋值,类型推断时如果无法确定具体类型会倾向于推断为any
。 - 应用方向:适用于快速开发阶段、原型设计、与无类型声明的 JavaScript 代码集成的过渡阶段,以及处理动态数据但暂时不关心其具体类型的场景。但在项目的核心业务逻辑和对稳定性要求高的部分,应避免使用
any
类型。
- 特点:
- unknown 类型特点与应用方向总结
- 特点:
unknown
类型表示任意类型,但 TypeScript 对其操作进行严格限制,需要通过类型缩小(如类型断言、typeof
检查、instanceof
检查等)才能安全地对其进行操作。任何类型的值可以赋值给unknown
类型变量,但unknown
类型变量不能直接赋值给其他具体类型变量,除非经过类型缩小。 - 应用方向:在处理来自不可信源的数据(如用户输入、第三方 API 响应)、函数接收可能是任意类型的参数且需要安全处理的场景,以及在强调类型安全和稳定性的项目中,
unknown
类型是更好的选择。它能帮助开发者在运行时确定值的类型,从而避免类型相关的错误,提高代码质量。
- 特点:
在实际的前端开发项目中,深入理解any
与unknown
类型的区别,并根据具体的业务需求和项目阶段合理选择使用,对于编写高质量、类型安全的 TypeScript 代码至关重要。通过不断实践和总结经验,我们可以更好地利用这两种类型的特性,提升开发效率和代码的可维护性。