TypeScript 类型设定
有多种方式设定变量的数据类型。
类型声明
在 TypeScript 中类型声明一般是在变量后使用 :
显式地指定数据类型(冒号的前后有没有空格都可以)。
let isDone: boolean = false;
此外对于函数,还可以同时约束输入和输出的数据类型,其中输入的约束在入参中设置,输出的约束在函数名(参数)后设置。
Tip
对于函数表达式,还可以为(在等号左侧)变量显式地进行约束。使用 =>
表示函数的定义,左边是输入类型,需要用括号 ()
括起来,右边是输出类型。
Tip
其中 TypeScript 的 void
类型就是是针对函数的无返回值的情况
// 函数声明
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 的规则(根据初始值)推断出一个类型。
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
以上代码虽然没有指定类型,但是会在编译的时候报错,因为事实上它等价于以下代码
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
Tip
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any
类型而完全不被类型检查
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
类型断言
除了通过显式的类型声明和隐式的类型推论来确定变量的数据类型,TypeScript 还提供类型断言来手动指定值的数据类型,通过 值 as 数据类型
或 <数据类型>值
两种方式,一般在以下情况中使用:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为
any
类型 any
类型可以被断言为任何类型
Tip
由于在 tsx 语法(React 的 jsx 语法的 ts 版)中,<Foo>
的语法表示的是一个 ReactNode
,因此必须使用 值 as 类型
来进行类型断言。因此建议统一使用 值 as 类型
这样的语法进行类型断言。
Warning
但是类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,滥用类型断言反而可能会导致运行时错误。因此使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。==所以为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as
语法更加优雅。==
联合类型指定唯一
如果 TypeScript 不确定一个联合类型的变量到底是哪个类型,则只能访问此联合类型的所有类型中共有的属性或方法,但是有时候确实需要访问其中一个类型特有的属性或方法,此时可以使用类型断言,将某个值指定为联合类型中的一种。
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
,编译时就不会报错
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
方法,就会导致运行时错误了。
父类断言为子类
当类之间有继承关系时,类型断言也是很常见,一般入参的数据类型是父类(为了更好的拓展性),但是有时候内部为了访问特定子类的属性时,此时才视情况再将值的类型断言为子类。
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
类型的变量上,访问任何属性都是允许的。
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
类型的值通过类型断言及时的指定为精确的类型(亡羊补牢 ),使我们的代码向着高可维护性的目标发展。
// 历史遗留的代码中有个 `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 兼容 B 或 B 兼容 A;相应地,若 A
兼容 B
,那么 A
能够被断言为 B
,B
也能被断言为 A
。==
Tip
TypeScript 是结构类型系统,即类型之间的关系对比只会比较它们的结构(当然会有更复杂的情况要考虑,具体查看类型兼容性这一章)。
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
。
interface Animal {
name: string;
}
interface Cat extends Animal {
run(): void;
}
因此 Cat
类型的 tom
可以赋值给 Animal
类型的 animal
,这就像我们可以将子类的实例赋值给类型为父类的变量。
当 Animal
兼容 Cat
时,它们就可以互相进行类型断言:
- 允许将父类型的值断言为子类
animal as Cat
,因为子类都继承了父类的属性,而且一般有自己独特的属性,这种断言相当于基于父类的约束,对该值进行更「细化」的约束 - ==允许将子类型的值断言为父类
cat as Animal
,因为既然子类拥有父类的属性和方法,那么被断言为父类,获取父类的属性、调用父类的方法,就不会有任何问题==
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
的父类,因此不能将父类的实例赋值给类型为子类的变量,因为父实例一般会缺少了子类型特有的属性。
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
兼容Cat
或Cat
兼容Animal
即可 - 值
animal
赋值给变量tom
(该变量约束为 Cat 类型),需要满足Cat
兼容Animal
才行,但是Cat
并不兼容Animal
。