系列文章目录
引入一:Typescript基础引入(基础类型、元组、枚举)
引入二:Typescript面向对象引入(接口、类、多态、重写、抽象类、访问修饰符)
第一章:Typescript基础知识(Typescript介绍、搭建TypeScript环境、基本数据类型)
第二章:Typescript常用类型(任意值any、数组Array、函数Function、元组Tuple、类型推论、联合类型)
第三章:Typescript基础知识(类型断言、类型别名、字符串字面量类型、枚举、交叉类型)
第四章:Typescript基础知识(类型拓宽、类型缩小)
第五章:TypeScript进阶知识之类(类的定义、类的基本使用、类的构造函数、类的属性和方法、访问修饰符、类的继承、抽象类)
第六章:TypeScript进阶知识之接口(接口定义、接口属性、可索引类型、接口表示函数类型、额外的属性检查、接口继承、接口与类型别名的区别)
第七章:TypeScript进阶知识之泛型(泛型的定义、为什么要使用泛型、泛型的使用、泛型变量、多个类型参数、泛型类、泛型接口、泛型参数默认类型、泛型约束)
文章目录
一、类型断言
言(Type Assertion)是 TypeScript 中的一种表达式,它可以用来告诉编译器一个值的确切类型。通过类型断言,我们可以在一些情况下主动指定变量的类型,以满足特定的需求。
1.1 语法
值 as 类型
或者
<类型>值
在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即值 as 类型
,因此我们更推荐使用 as 语法。
形如 的语法在 tsx 中表示的是一个 ReactNode,在ts中除了表示类型断言之外,也可能是表示一个泛型。
1.2 断言的用途
1.2.1 将一个联合类型断言为其中一个类型
- 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们
只能访问此联合类型的所有类型中共有的属性或方法
。 - 当我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法时,就会报错。
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name;
}
function isFish(animal: Cat | Fish) {
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'.
此时可以使用类型断言,将 animal 断言成 Fish:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
1.2.2 将一个父类断言为更加具体的子类
当类之间有继承关系时,类型断言也是很常见:
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
上述例子中,由于父类 Error 中没有 code 属性,故直接获取 error.code 会报错,需要使用类型断言获取 (error as ApiError).code。
上述例子中有一个更合适的方式来判断是不是 ApiError,那就是使用 instanceof:
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
但是当 ApiError 和 HttpError 不是一个真正的类
,而是一个 TypeScript 的接口
(interface),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,就无法使用 instanceof 来做运行时判断,此时就只能用类型断言
,通过判断是否存在 code 属性,来判断传入的参数是不是 ApiErro:
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
/*
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
*/
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number' ) {
return true;
}
return false;
}
1.2.3 将任何一个类型断言为any
当我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
此时我们可以使用 as any 临时将 window 断言为 any 类型:
(window as any).foo = 1;
注意: 将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any。
1.2.4 将 any 断言为一个具体的类型
开发中,我们不可避免的需要处理 any 类型的变量,遇到 any 类型的变量时,我们可以选择无视它,任由它滋生更多的 any。
我们也可以选择改进它,通过类型断言及时的把 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();
1.3 非空断言运算符( ! 后缀)
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。 !
在任何表达式之后写入
代表一种类型断言,即该值不是 null or undefined
:
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // error
function liveDangerously(x?: number | null) {
console.log(x!.toFixed());
}
1.4 确定赋值断言
允许在实例属性和变量声明后面放置一个 ! 号
,从而告诉 TypeScript 该属性会被明确地赋值
。
let x: number;
initialize();
console.log(2 * x); // Variable 'x' is used before being assigned.(2454)
function initialize() {
x = 10;
}
上述例子中,该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言
let x!: number;
initialize();
console.log(2 * x);
function initialize() {
x = 10;
}
二、类型别名
作用:
类型别名用来给一个类型起个新名字
(类型别名常用于联合类型)
用法:
使用 type 关键字可以为任何类型定义别名,创建类型别名后,直接使用该类型别名作为变量的类型注解即可。
注意:
- 尽量选择有意义的别名,能够准确描述类型的用途,提高代码的可读性。
- 避免过度使用类型别名,过多的别名可能导致代码的可维护性变差。
- 注意避免循环引用的情况,即在类型别名中引用自身,这会导致编译错误。
- 类型别名并不创建新的类型,所以它无法被继承或实现。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
function test() {
return "hello";
}
console.log(getName("zs"));//zs
console.log(getName(test));//hello
三、枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景.枚举使用 enum
关键字来定义。
枚举成员会被赋值为从 0 开始递增的数字
,同时也会对枚举值到枚举名进行反向映射
。
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
console.log(Days);
/*
{
'0': 'Mon',
'1': 'Tue',
'2': 'Wed',
'3': 'The',
'4': 'Fri',
'5': 'Sat',
Sun: 'text',
Mon: 0,
Tue: 1,
Wed: 2,
The: 3,
Fri: 4,
Sat: 5
}
*/
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
var Days;
(function (Days) {
Days[Days["Sun"] = 0] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["The"] = 4] = "The";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
console.log(Days);
console.log(Days.Sun);
console.log(Days["Mon"]);
console.log(Days["The"]);
console.log(Days[0]);
console.log(Days[1]);
console.log(Days[4]);
3.1 手动赋值
我们也可以给枚举项手动赋值,未手动赋值的枚举项会接着上一个枚举项递增
。如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
上面的例子中,递增到 3 的时候与前面的 Sun 的取值重复了,但是 TypeScript 并没有报错,导致 Days[3] 的值先是 “Sun”,而后又被 “Wed” 覆盖了。编译的结果是:
var Days;
(function (Days) {
Days[Days["Sun"] = 3] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
所以使用的时候需要注意,最好不要出现这种覆盖的情况。
手动赋值的枚举项可以不是数字,可以是其他类型:
enum Color {
Red,
Green,
Blue = "blur",
}
console.log(Color); //{ '0': 'Red', '1': 'Green', Red: 0, Green: 1, Blue: 'blur' }
四、交叉类型
交叉类型是将多个类型合并为一个类型
。它包含了所需的所有类型的特性,使用&定义交叉类型
。
{
type Useless = string & number;
}
如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何意义的。在上述的代码中,类型别名 Useless 的类型就是个 never。
交叉类型真正的用处是将多个接口类型合并成一个类型,从而实现等同接口继承的效果
,也就是所谓的合并接口类型,如下代码所示:
type IntersectionType = { id: number; name: string; } & { age: number };
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
在上述示例中,我们通过交叉类型,使得 IntersectionType 同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。
五、字面量类型
5.1 字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
type Direction = 'up' | 'down';
function move(dir: Direction) {
console.log(dir)
}
move('up'); // up
move('right'); // error:Argument of type '"right"' is not assignable to parameter of type 'Direction'
相较于使用 string 类型,使用字面量类型可以将函数的参数限定为更具体的类型
。这不仅提升了程序的可读性,还保证了函数的参数类型,可谓一举两得。
5.2 数字字面量类型及布尔字面量类型
数字字面量类型和布尔字面量类型的使用与字符串字面量类型的使用类似,我们可以使用字面量组合的联合类型将函数的参数限定为更具体的类型:
interface Config {
size: 'small' | 'big';
isEnable: true | false;
margin: 0 | 2 | 4;
}
let conf: Config = {
size: "big",
isEnable: true,
margin: 1,//error: Type '1' is not assignable to type '0 | 2 | 4'
};
5.3 关于let和const的字面量类型分析
我们先来看一个示例,如下代码所示:
{
const str = 'this is string'; // str: 'this is string'
const num = 1; // num: 1
const bool = true; // bool: true
let str1 = 'this is string'; // str: string
let num2 = 1; // num: number
let bool3 = true; // bool: boolean
}
- 在上述代码中,我们将 const 定义为一个不可变更的常量,在缺少类型注解的情况下,TypeScript推断出它的类型直接转化为
赋值字面量的类型
。 - 我们将let定义为一个可变更的变量,缺少类型注解的变量的类型转换为了
赋值字面量类型的父类型
,比如 str1的类型是 ‘this is string’ 类型(这里表示一个字符串字面量类型)的父类型 string,num1 的类型是 1 类型的父类型 number。
我们将 TypeScript 的字面量子类型转换为父类型的这种设计称之为**字面量类型的拓宽**,下一章内容会着重进行介绍。