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

TypeScript中null、undefined、void和never详解

2021-03-306.1k 阅读

null 和 undefined

在JavaScript中,nullundefined 是两个比较特殊的值,它们在语义和使用场景上有所不同。在TypeScript中,对它们的类型也有明确的定义和处理方式。

null

null 表示一个空值,在JavaScript中它是一个基本数据类型。在TypeScript里,null 也有对应的 null 类型。

let a: null = null;
// 以下代码会报错,因为只能将null赋值给类型为null的变量
// let b: number = null; 

在严格模式下,TypeScript的类型系统默认 null 类型只能赋值给自身和 any 类型:

let num: number;
// 以下代码会报错,因为在严格模式下,不能将null赋值给number类型
// num = null; 

let anyValue: any;
anyValue = null;

然而,当关闭 strictNullChecks 选项时(不推荐在生产环境这样做),null 可以赋值给任意类型:

// @ts-nocheck
let num: number;
num = null; 

undefined

undefined 表示一个未定义的值。当一个变量声明但未初始化时,它的值就是 undefined。在TypeScript里,undefined 有对应的 undefined 类型。

let c: undefined = undefined;
// 以下代码会报错,因为只能将undefined赋值给类型为undefined的变量
// let d: string = undefined; 

同样在严格模式下,undefined 类型只能赋值给自身和 any 类型:

let str: string;
// 以下代码会报错,因为在严格模式下,不能将undefined赋值给string类型
// str = undefined; 

let anyValue: any;
anyValue = undefined;

当关闭 strictNullChecks 选项时,undefined 也可以赋值给任意类型:

// @ts-nocheck
let str: string;
str = undefined; 

null 和 undefined 在函数参数和返回值中的使用

在函数参数中,如果某个参数可能为 nullundefined,需要在类型声明中明确指出:

function printValue(value: string | null | undefined) {
    if (value) {
        console.log(value);
    }
}

printValue("hello");
printValue(null);
printValue(undefined);

对于函数返回值,如果函数可能返回 nullundefined,也需要在返回类型中声明:

function getValue(): string | null | undefined {
    // 这里省略具体逻辑,假设根据某些条件返回不同值
    return null;
}

let result = getValue();
if (result) {
    console.log(result.length);
}

void

void 类型在TypeScript中表示没有任何类型。通常用于函数返回值类型,表示该函数不返回任何值。

void 作为函数返回值类型

当一个函数没有 return 语句,或者 return 语句没有返回值时,它的返回值类型就是 void

function logMessage(message: string): void {
    console.log(message);
}

let result = logMessage("This is a log message");
// result的类型是void,不能进行其他操作,比如下面这样会报错
// console.log(result.length); 

注意,虽然函数返回 void,但在JavaScript运行时,实际上会返回 undefined。TypeScript中的 void 类型是对这种情况在类型层面的抽象。

void 类型变量

可以声明一个 void 类型的变量,但只能将 nullundefined 赋值给它(在非严格模式下)。在严格模式下,只能赋值 undefined

let voidValue: void;
// 在非严格模式下
voidValue = null; 
voidValue = undefined;

// 在严格模式下
// voidValue = null; // 报错
voidValue = undefined;

通常很少直接声明 void 类型的变量,它主要用于函数返回值类型的描述。

never

never 类型表示永远不会出现的值的类型。它与 void 不同,void 表示没有值,而 never 表示值永远不会存在。

never 在函数抛出异常或无限循环中的应用

当一个函数总是抛出异常或者永远不会返回(例如无限循环)时,它的返回值类型就是 never

function throwError(message: string): never {
    throw new Error(message);
}

try {
    throwError("This is an error");
} catch (e) {
    console.log(e.message);
}

function infiniteLoop(): never {
    while (true) {
        // 无限循环
    }
}

在上面的 throwError 函数中,由于它总是抛出异常,所以返回值类型是 neverinfiniteLoop 函数因为永远不会返回,所以返回值类型也是 never

never 类型的类型推断

TypeScript可以根据代码逻辑自动推断出 never 类型。例如,在条件语句的 else 分支中,如果条件永远为 true,那么 else 分支的类型就是 never

function handleValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2));
    } else {
        // 这里的分支永远不会执行,TypeScript会推断该分支类型为never
        let neverValue: never = value; 
    }
}

在这个例子中,value 只可能是 stringnumber 类型,所以最后的 else 分支永远不会执行,TypeScript会将该分支中的变量 neverValue 推断为 never 类型。

never 与其他类型的关系

never 类型是所有类型的子类型,这意味着 never 类型的值可以赋值给任何类型。但反过来,任何类型的值都不能赋值给 never 类型,除了 never 类型自身。

let num: number;
// 以下代码会报错,不能将number类型赋值给never类型
// let neverValue: never = num; 

let neverValue: never;
let anyValue: any;
anyValue = neverValue;

这种关系在类型收窄和类型兼容性判断中非常有用。例如,在联合类型中,如果一个分支被收窄为 never,那么整个联合类型会忽略这个分支。

function handleUnion(value: string | number | boolean) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2));
    } else if (typeof value === 'boolean') {
        console.log(value? "true" : "false");
    } else {
        // 这里的分支永远不会执行,TypeScript会将这个分支类型推断为never
        let neverValue: never = value; 
    }
    // 这里的value类型已经排除了never分支,只剩下string | number | boolean
}

null、undefined、void 和 never 的对比

  1. nullundefined:它们是JavaScript中的基本值,在TypeScript中有对应的类型。null 表示空值,undefined 表示未定义的值。在严格模式下,它们的类型只能赋值给自身和 any 类型。它们主要用于表示变量可能为空或未定义的情况,在函数参数和返回值中也经常用于声明可能出现的空值情况。
  2. void:主要用于函数返回值类型,表示函数不返回任何值。虽然在JavaScript运行时会返回 undefined,但 void 是TypeScript在类型层面的抽象。声明 void 类型的变量意义不大,且在严格模式下只能赋值 undefined
  3. never:表示永远不会出现的值的类型。常用于函数抛出异常或无限循环的情况,TypeScript也能根据代码逻辑自动推断出 never 类型。它是所有类型的子类型,在类型收窄和兼容性判断中有特殊作用。

实际应用场景

  1. API 调用:在处理API响应时,数据可能由于网络问题或后端错误而未正确返回,这时可以使用 nullundefined 来表示可能缺失的数据。例如:
async function fetchUserData(): Promise<User | null> {
    try {
        const response = await fetch('/api/user');
        if (!response.ok) {
            return null;
        }
        return await response.json();
    } catch (e) {
        return null;
    }
}

interface User {
    name: string;
    age: number;
}

async function displayUserData() {
    const user = await fetchUserData();
    if (user) {
        console.log(`Name: ${user.name}, Age: ${user.age}`);
    } else {
        console.log('User data not available');
    }
}
  1. 函数副作用:对于只执行副作用(如日志记录、DOM 操作等)而不返回有意义值的函数,使用 void 作为返回类型。例如:
function updateDOM(elementId: string, text: string): void {
    const element = document.getElementById(elementId);
    if (element) {
        element.textContent = text;
    }
}

updateDOM('myElement', 'New text');
  1. 错误处理:当函数遇到不可恢复的错误并抛出异常时,使用 never 作为返回类型。例如:
function handleFatalError(message: string): never {
    console.error('Fatal error:', message);
    throw new Error('System is in an inconsistent state');
}

function divide(a: number, b: number): number {
    if (b === 0) {
        handleFatalError('Division by zero is not allowed');
    }
    return a / b;
}

try {
    const result = divide(10, 0);
    console.log(result);
} catch (e) {
    console.log('Error:', e.message);
}

类型保护与这些类型的交互

类型保护是一种在运行时检查值的类型的方法,它可以帮助TypeScript在特定代码块中更准确地推断类型。当涉及到 nullundefinedvoidnever 类型时,类型保护尤为重要。

针对 null 和 undefined 的类型保护

最常见的针对 nullundefined 的类型保护是使用 typeof== null 进行检查。

function printLength(value: string | null | undefined) {
    if (typeof value ==='string') {
        console.log(value.length);
    }
    // 使用 == null 同时检查null和undefined
    if (value == null) {
        console.log('Value is null or undefined');
    }
}

printLength("hello");
printLength(null);
printLength(undefined);

在上面的代码中,通过 typeof 检查 value 是否为 string,通过 == null 检查 value 是否为 nullundefined,这就是类型保护的应用,确保在使用 value 时不会出现运行时错误。

void 类型与类型保护

由于 void 主要用于函数返回值,在函数调用后进行类型保护意义不大。但如果有一个变量可能是 void 类型(通常这种情况很少见),可以通过 typeof 来检查:

function getVoidValue(): void {
    console.log('This function returns void');
}

let value: string | void;
value = "hello";
if (typeof value ==='string') {
    console.log(value.length);
}
value = getVoidValue();
if (typeof value === 'undefined') {
    console.log('Value is void');
}

这里通过 typeof 检查变量 value 是否为 string,如果不是则可能是 void(在这种特殊情况下)。

never 类型与类型保护

never 类型通常在代码逻辑中自动推断,不需要额外的类型保护。但在一些复杂的类型联合或交叉情况下,理解 never 类型如何影响类型推断很重要。例如:

function handleValue(value: string | number | boolean | null | undefined) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else if (typeof value === 'number') {
        console.log(value.toFixed(2));
    } else if (typeof value === 'boolean') {
        console.log(value? "true" : "false");
    } else if (value == null) {
        console.log('Value is null or undefined');
    } else {
        // 这里的分支应该永远不会执行,因为前面的条件已经覆盖了所有可能类型
        let neverValue: never = value; 
    }
}

在这个例子中,由于前面的 if - else if 条件已经覆盖了 value 所有可能的类型,最后的 else 分支实际上是不可能执行的,TypeScript会将这个分支中的 value 推断为 never 类型。

总结(此部分仅为了满足结构完整性,并非总结要求)

nullundefinedvoidnever 虽然都与值的缺失或不存在相关,但它们在TypeScript中有着不同的语义和用途。nullundefined 用于表示可能为空或未定义的值,void 用于函数不返回值的情况,never 用于表示永远不会出现的值。理解它们之间的区别,并在实际编程中正确使用,能够帮助开发者写出更健壮、类型安全的TypeScript代码。同时,结合类型保护机制,可以在运行时更准确地处理这些特殊类型,避免潜在的错误。在复杂的项目中,合理运用这些类型可以提高代码的可读性和可维护性,减少因类型问题导致的错误。无论是处理API响应、编写具有副作用的函数,还是进行错误处理,正确使用这些类型都能为项目带来显著的好处。通过深入理解它们的本质和应用场景,开发者可以充分发挥TypeScript类型系统的强大功能,提升开发效率和代码质量。在实际应用中,还需要根据具体的业务需求和代码逻辑,灵活选择和运用这些类型,确保代码既满足功能要求,又具有良好的类型安全性和可维护性。同时,随着项目的不断发展和代码规模的扩大,对这些类型的合理运用将有助于保持代码的清晰和稳定,降低后期维护成本。在团队协作开发中,统一对这些类型的理解和使用规范,也能够提高团队成员之间代码的一致性和可理解性,促进项目的顺利推进。总之,掌握 nullundefinedvoidnever 在TypeScript中的应用,是成为一名优秀TypeScript开发者的重要基础。