TypeScript 类型设定

typescript

TypeScript 类型设定

有多种方式设定变量的数据类型。

类型声明

在 TypeScript 中类型声明一般是在变量后使用 : 显式地指定数据类型(冒号的前后有没有空格都可以)。

ts
let isDone: boolean = false;

此外对于函数,还可以同时约束输入和输出的数据类型,其中输入的约束在入参中设置,输出的约束在函数名(参数)后设置。

Tip

对于函数表达式,还可以为(在等号左侧)变量显式地进行约束。使用 => 表示函数的定义,左边是输入类型,需要用括号 () 括起来,右边是输出类型。

Tip

其中 TypeScript 的 void 类型就是是针对函数的无返回值的情况

ts
// 函数声明
function sum(x: number, y: number): number {
    return x + y;
}
// 函数表达式
let mySum = function (x: number, y: number): number {
    return x + y;
};
// 对赋值指向函数表达式的变量也进行约束
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};
Tip

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型

  • 函数的合并:使用重载定义多个函数类型,在使用时 TypeScript 会从上往下进行匹配
    ts
    function reverse(x: number): number;
    function reverse(x: string): string;
    function reverse(x: number | string): number | string {
        if (typeof x === 'number') {
            return Number(x.toString().split('').reverse().join(''));
        } else if (typeof x === 'string') {
            return x.split('').reverse().join('');
        }
    }
  • 接口的合并:简单的合并到一个接口中,对于接口中的方法的合并,与函数的合并一样(重载)
    ts
    interface Alarm {
        price: number;
    }
    interface Alarm {
        weight: number;
    }
    // 相当于
    interface Alarm {
        price: number;
        weight: number;
    }
    Warning

    合并的属性的类型必须是唯一

    ts
    /*
     * 类型唯一
     */
    interface Alarm {
        price: number;
    }
    interface Alarm {
        price: number;  // 虽然重复了,但是类型都是 `number`,所以不会报错
        weight: number;
    }
    /*
     * 类型不唯一
     */
    interface Alarm {
        price: number;
    }
    interface Alarm {
        price: string;  // 类型不一致,会报错
        weight: number;
    }
    // index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type.  Variable 'price' must be of type 'number', but here has type 'string'.
  • 类的合并:与接口的合并一致

类型推论

如果在定义变量时,没有进行类型声明,即明确指定变量的数据类型,那么 TypeScript 会依照类型推论 Type Inference 的规则(根据初始值)推断出一个类型。

ts
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

以上代码虽然没有指定类型,但是会在编译的时候报错,因为事实上它等价于以下代码

ts
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
Tip

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

ts
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

类型断言

除了通过显式的类型声明和隐式的类型推论来确定变量的数据类型,TypeScript 还提供类型断言来手动指定的数据类型,通过 值 as 数据类型<数据类型>值 两种方式,一般在以下情况中使用:

  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为 any 类型
  • any 类型可以被断言为任何类型
Tip

由于在 tsx 语法(React 的 jsx 语法的 ts 版)中,<Foo> 的语法表示的是一个 ReactNode,因此必须使用 值 as 类型 来进行类型断言。因此建议统一使用 值 as 类型 这样的语法进行类型断言。

Warning

但是类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,滥用类型断言反而可能会导致运行时错误。因此使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。==所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅。==

联合类型指定唯一

如果 TypeScript 不确定一个联合类型的变量到底是哪个类型,则只能访问此联合类型的所有类型中共有的属性或方法,但是有时候确实需要访问其中一个类型特有的属性或方法,此时可以使用类型断言,将某个值指定为联合类型中的一种。

ts
interface Cat {
    name: string;
    run: () => void; // 约束对象的方法 run
}
interface Fish {
    name: string;
    swim: () => void;
}
function isFish(animal: Cat | Fish) {
    // animal 是联合类型,未进行类型断言,在编译时会报错
    if (typeof animal.swim === 'function') {
        return true;
    }
    return false;
}
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
function isFish(animal: Cat | Fish) {
    // 对 animal 进行类型断言,指定为 Fish 数据类型,可以访问 Fish 数据类型特有的属性
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

以上示例中函数 isFish 的入参 animal 原来是联合类型,但是为了在内部可以访问 Fish 类型特有的方法 swim,所以通过类型断言将它约束为类型 Fish,编译时就不会报错

ts
interface Cat {
    name: string;
    run: () => void;
}
interface Fish {
    name: string;
    swim: () => void;
}
function swim(animal: Cat | Fish) {
    (animal as Fish).swim();
}
const tom: Cat = {
    name: 'Tom',
    run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`

以上示例在编译时不会报错,但在运行时会报错,因为 (animal as Fish).swim() 这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。可是 swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误了。

父类断言为子类

当类之间有继承关系时,类型断言也是很常见,一般入参的数据类型是父类(为了更好的拓展性),但是有时候内部为了访问特定子类的属性时,此时才视情况再将值的类型断言为子类。

ts
interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}
function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

以上示例中声明了函数 isApiError 用来判断传入的参数是不是 ApiError 类型,为了实现这样一个函数,入参的类型肯定是比较抽象的父类 Error,这样的话函数就可以接受 Error,当然也能是它的子类,所以在函数内部的核心代码中进行判断。

但是由于父类 Error 中没有 code 属性,故直接获取 error.code 会报错,因此需要在内部对 error 该值使用类型断言以获取 (error as ApiError).code 访问权限

断言为 any

理想情况下 TypeScript 中每个值的类型都具体而精确,当我们引用一个在此类型上不存在的属性或方法时,就会在编译阶段报错提示开发者。

但有时候我们需要访问一个对象属性,例如 window,如果未显性的声明该对象的「形状」,直接访问它的属性会在编译时报错。有很多解决方法,但是最简便直接的方法是临时将 window 断言为 any 类型,因为在 any 类型的变量上,访问任何属性都是允许的

ts
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
(window as any).foo = 1;
Warning

将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any

将 any 断言为唯一

由于声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值,为了避免滋生更多的 any,可以将一些本来是 any 类型的值通过类型断言及时的指定为精确的类型(亡羊补牢 ),使我们的代码向着高可维护性的目标发展。

ts
// 历史遗留的代码中有个 `getCacheData`,它的返回值是 `any`
function getCacheData(key: string): any {
    return (window as any).cache[key];
}
interface Cat {
    name: string;
    run: () => void;
}
// 使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作
const tom = getCacheData('tom') as Cat;
tom.run();

断言的限制

任何一个类型并能无限制被断言为任何另一个类型,==要使得 A 能够被断言为 B,需要 A 兼容 BB 兼容 A;相应地,若 A 兼容 B,那么 A 能够被断言为 BB 也能被断言为 A。==

Tip

TypeScript 是结构类型系统,即类型之间的关系对比只会比较它们的结构(当然会有更复杂的情况要考虑,具体查看类型兼容性这一章)。

ts
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}
let tom: Cat = {
    name: 'Tom',
    run: () => { console.log('run') }
};
let animal: Animal = tom;

在上面的例子中,Cat 接口定义的对象「形状」包含了 Animal 接口定义的对象「形状」,只是它还有一个额外的方法 run。TypeScript 基于两个接口的结构判定两者的关系,所以两者的关系其实与 Cat extends Animal 是等价的,这在 TypeScript 中更专业的说法是:Animal 兼容 Cat

ts
interface Animal {
    name: string;
}
interface Cat extends Animal {
    run(): void;
}

因此 Cat 类型的 tom 可以赋值给 Animal 类型的 animal,这就像我们可以将子类的实例赋值给类型为父类的变量。

Animal 兼容 Cat 时,它们就可以互相进行类型断言:

  • 允许将父类型的值断言为子类 animal as Cat,因为子类都继承了父类的属性,而且一般有自己独特的属性,这种断言相当于基于父类的约束,对该值进行更「细化」的约束
  • ==允许将子类型的值断言为父类 cat as Animal,因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题==
ts
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}
function testAnimal(animal: Animal) {
    return (animal as Cat);
}
function testCat(cat: Cat) {
    return (cat as Animal);
}
Warning

Animal 可以看作是 Cat 的父类,因此不能将父类的实例赋值给类型为子类的变量,因为父实例一般会缺少了子类型特有的属性。

ts
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}
const animal: Animal = {
    name: 'tom'
};
let tom: Cat = animal;
// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.

深入的讲它们的核心区别就在于:

  • animal 断言为 Cat,只需要满足 Animal 兼容 CatCat 兼容 Animal 即可
  • animal 赋值给变量 tom(该变量约束为 Cat 类型),需要满足 Cat 兼容 Animal 才行,但是 Cat 并不兼容 Animal

Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes