sss

TypeScript类型缩小

Quber...大约 4 分钟TypeScript

TypeScript 变量的值可以变,但是类型通常是不变的。唯一允许的改变,就是类型缩小,就是将变量值的范围缩得更小。

1、手动类型缩小

如果一个变量属于联合类型,所以使用时一般需要缩小类型。

第一种方法是使用if判断。

function getScore(value: number | string): number {
    if (typeof value === "number") {
        // (A)
        // %inferred-type: number
        value;
        return value;
    }
    if (typeof value === "string") {
        // (B)
        // %inferred-type: string
        value;
        return value.length;
    }
    throw new Error("Unsupported value: " + value);
}

如果一个值是anyunknown,你又想对它进行处理,就必须先缩小类型。

function parseStringLiteral(stringLiteral: string): string {
    const result: unknown = JSON.parse(stringLiteral);
    if (typeof result === "string") {
        // (A)
        return result;
    }
    throw new Error("Not a string literal: " + stringLiteral);
}

下面是另一个例子。

interface Book {
    title: null | string;
    isbn: string;
}

function getTitle(book: Book) {
    if (book.title === null) {
        // %inferred-type: null
        book.title;
        return "(Untitled)";
    } else {
        // %inferred-type: string
        book.title;
        return book.title;
    }
}

缩小类型的前提是,需要先获取类型。获取类型的几种方法如下。

function func(value: Function | Date | number[]) {
    if (typeof value === "function") {
        // %inferred-type: Function
        value;
    }

    if (value instanceof Date) {
        // %inferred-type: Date
        value;
    }

    if (Array.isArray(value)) {
        // %inferred-type: number[]
        value;
    }
}

1.2、typeof 运算符

第二种方法是使用switch缩小类型。

function getScore(value: number | string): number {
    switch (typeof value) {
        case "number":
            // %inferred-type: number
            value;
            return value;
        case "string":
            // %inferred-type: string
            value;
            return value.length;
        default:
            throw new Error("Unsupported value: " + value);
    }
}

1.3、instanceof 运算符

第三种方法是 instanceof 运算符。它能够检测实例对象与构造函数之间的关系。instanceof 运算符的左操作数为实例对象,右操作数为构造函数,若构造函数的 prototype 属性值存在于实例对象的原型链上,则返回 true;否则,返回 false。

function f(x: Date | RegExp) {
    if (x instanceof Date) {
        x; // Date
    }

    if (x instanceof RegExp) {
        x; // RegExp
    }
}

instanceof 类型守卫同样适用于自定义构造函数,并对其实例对象进行类型细化。

class A {}
class B {}

function f(x: A | B) {
    if (x instanceof A) {
        x; // A
    }

    if (x instanceof B) {
        x; // B
    }
}

1.4、in 运算符

第四种方法是使用 in 运算符。

in 运算符是 JavaScript 中的关系运算符之一,用来判断对象自身或其原型链中是否存在给定的属性,若存在则返回 true,否则返回 false。in 运算符有两个操作数,左操作数为待测试的属性名,右操作数为测试对象。

in 类型守卫根据 in 运算符的测试结果,将右操作数的类型细化为具体的对象类型。

interface A {
    x: number;
}
interface B {
    y: string;
}

function f(x: A | B) {
    if ("x" in x) {
        x; // A
    } else {
        x; // B
    }
}
interface A {
    a: number;
}
interface B {
    b: number;
}
function pickAB(ab: A | B) {
    if ("a" in ab) {
        ab; // Type is A
    } else {
        ab; // Type is B
    }
    ab; // Type is A | B
}

缩小对象的属性,要用in运算符。

type FirstOrSecond = { first: string } | { second: string };

function func(firstOrSecond: FirstOrSecond) {
    if ("second" in firstOrSecond) {
        // %inferred-type: { second: string; }
        firstOrSecond;
    }
}

// 错误
function func(firstOrSecond: FirstOrSecond) {
    // @ts-expect-error: Property 'second' does not exist on
    // type 'FirstOrSecond'. [...]
    if (firstOrSecond.second !== undefined) {
        // ···
    }
}

in运算符只能用于联合类型,不能用于检查一个属性是否存在。

function func(obj: object) {
    if ("name" in obj) {
        // %inferred-type: object
        obj;

        // 报错
        obj.name;
    }
}

1.5、特征属性

对于不同对象之间的区分,还可以人为地为每一类对象设置一个特征属性。

interface UploadEvent {
    type: "upload";
    filename: string;
    contents: string;
}
interface DownloadEvent {
    type: "download";
    filename: string;
}
type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
    switch (e.type) {
        case "download":
            e; // Type is DownloadEvent
            break;
        case "upload":
            e; // Type is UploadEvent
            break;
    }
}

2、any 类型的细化

TypeScript 推断变量类型时,会根据获知的信息,不断改变推断出来的类型,越来越细化。这种现象在any身上特别明显。

function range(start: number, limit: number) {
    const out = []; // 类型为 any[]
    for (let i = start; i < limit; i++) {
        out.push(i);
    }
    return out; // 类型为 number[]
}

上面示例中,变量out的类型一开始推断为any[],后来在里面放入数值,类型就变为number[]

再看下面的例子。

const result = []; // 类型为 any[]
result.push("a");
result; // 类型为 string[]
result.push(1);
result; // 类型为 (string | number)[]

上面示例中,数组result随着成员类型的不同,而不断改变自己的类型。

注意,这种any类型的细化,只在打开了编译选项noImplicitAny时发生。

这时,如果在变量的推断类型还为any时(即没有任何写操作),就去输出(或读取)该变量,则会报错,因为这时推断还没有完成,无法满足noImplicitAny的要求。

const result = []; // 类型为 any[]
console.log(typeof result); // 报错
result.push("a"); // 类型为 string[]

上面示例中,只有运行完第三行,result的类型才能完成第一次推断,所以第二行读取result就会报错。

3、is 运算符

is运算符返回一个布尔值,用来判断左侧的值是否属于右侧的类型。

function isInputElement(el: HTMLElement): el is HTMLInputElement {
    return "value" in el;
}

function getElementContent(el: HTMLElement) {
    if (isInputElement(el)) {
        el; // Type is HTMLInputElement
        return el.value;
    }
    el; // Type is HTMLElement
    return el.textContent;
}
function isDefined<T>(x: T | undefined): x is T {
    return x !== undefined;
}