公众号:CS阿吉
1. 简介
TypeScript具有类型系统,且是JavaScript的超集。 它可以编译成普通的JavaScript代码。 TypeScript支持任意浏览器,任意环境,任意系统并且是开源的。
TypeScript 通过类型注解对变量类型进行约束。
TypeScript 是编译期行为,它不引入额外开销,同时没有改变运行时。
TypeScript 的类型系统是结构化的,即Structral Subtyping
,这意味着是对值的约束,而非标识符。即在TS中,只要两个对象的结构、属性、方法的类型均一致,则类型一致。
进行类型比较时,结构化类型 Structral Subtyping 比较类型结构相同,则相同。具名类型 Naming Type 比较类型标识符相同,则相同。
interface Foo {
name: string;
}
interface Bar {
name: string;
}
let foo: Foo = { name: '1' };
let bar: Bar = { name: '2' };
foo = bar; // success
// Naming Type 会认为 Foo 和 Bar 不相同,会报错。但TS编译正常。
2. 安装TS环境
$ npm i -g typescript # 安装 ts
$ tsc --init # 生成配置文件 tsconfig.json
$ npm install @types/node # 获取类型声明文件
$ tsc filename.ts # 编译,默认会在和 filename.ts 同级目录下生成一个同名的 .js 文件
3. 变量类型
在 JavaScript 中提供了 7种
数据类型(BigInt类型尚在提案阶段,虽然大部分浏览器已经支持,但尚未纳入标准)。
注:TS也支持bigint。虽然number和bigint都表示数字,但这两个类型不兼容。
TS中依然有上述7种
类型。TS的作用是将JS类型化,在 JavaScript 的基础上添加了静态类型注解扩展。如果TS丧失其应有的作用,即对变量没有类型约束,则使用 any类型
将TS退化为JS。同时,TS增加了 5种
类型,分别是 array、tuple、enum、void、never、unknown。
综上所述,在 TypeScript 中提供了 14种
数据类型,如下:
•1.any(任意类型)
•2.boolean(布尔类型)
•3.number(数字类型)
•4.string(字符串类型)
•5.null 类型
•6.undefined 类型
•7.symbol 类型
•8.object 对象类型
•9.array(数组类型)
•10.tuple(元组类型)
•11.enum(枚举类型)
•12.void 类型
•13.never 类型
•14.unknown 类型
3-0. 知识扩展延伸
TypeScript也具备 Number、String、Boolean、Symbol 等类型(注意不是number、string等)。
在TS中,number和Number是两种数据类型,不可以等价。测试如下:
let a: String = "S";
let b: string = "s";
a = b; // success
b = a; // error TS2322: Type 'String' is not assignable to type 'string'.
// 'string' is a primitive, but 'String' is a wrapper object. Prefer using 'string' when possible.
在实际的项目开发中,根本用不到 Number、String、Boolean、Symbol 类型,也没有什么特殊用途。之所以用不上,就好比我们不必用 Number、String、Boolean、Symbol 构造函数去 new 实例一样。
3-1. any(任意类型)
any(任意类型)可表示任何类型的值,即无约束。any 可绕过静态类型检测。
any 通常用于声明 DOM 节点,因为 DOM 节点并不是我们通常意义上说的对象,即不是真正的对象。
let a: any = 0;
a = '';
a = {};
/**
* 一个 DOMValue 代表某个 DOM 值 / DOM 操作;
* 因为概念太宽泛, 索性定义为 any 了
*/
export type DOMValue = any;
在TS项目中,any用的越多,TS越像JS。因此建议,通常情况下避免使用any。如果一定要使用,那么推荐使用unknown。
我们可以对 any类型 的值进行任意操作,无需在编译阶段进行类型检查。TS 3.0 引入的 unknown类型 是 any类型 的安全类型,对 unknown类型 的值进行操作之前,需要检查其类型,因为 unknown类型 只能赋值给 any类型 和 unknown类型。
3-2. boolean(布尔类型)
let canRun: boolean = true;
let isSelected: boolean = false;
isSelected = 1; // error TS2322: Type 'number' is not assignable to type 'boolean'.
3-3. number(数字类型)
在TS中,number类型同JS一样,支持十进制整数、浮点数、二进制数、八进制数、十六进制数。
let integer: number = 10;
let floatingPoint: number = 0.01;
let binary: number = 0b1111;
let octal: number = 0o111;
let hex: number = 0x1111;
注意:Infinity, -Infinity, NaN 也属于Number类型。
3-4. string(字符串类型)
在JS中支持的字符串定义方法均可被string类型约束。
let s1: string = 'csaj';
let s2: string = `${s1} - csaj`;
let s3: string = String('csaj');
3-5. null 类型
null 类型只能被被赋值为null。
null类型 常用于前后端交互的接口制定,其表明对象或属性可能是空值。
interface HomeInfoRsp {
roomId: number;
roomName: string;
}
type HomeInfoState = {
loaded: boolean;
data: null | HomeInfoRsp;
loading: boolean;
error: any;
};
3-6. undefined 类型
undefined 类型的数据只能被赋值为 undefined。
undefined类型 常用于定义接口类型,其表示可缺省、未定义的属性。
interface ItemProps {
name: string;
description?: string;
}
undefined 类型可赋值给 void 类型,但 void 类型不可赋值给 undefined 类型。测试如下:
let undeclared: undefined = undefined;
let unusable: void = undefined;
unusable = undeclared; // success
undeclared = unusable; // error TS2322: Type 'void' is not assignable to type 'undefined'.
undefined类型、null类型是其他所有类型的子类型,即 undefined类型、null类型 可被赋值给 其他所有类型。
3-7. symbol 类型
symbol类型可通过Symbol构造函数创建。
let s1: symbol = Symbol();
let s2: symbol = Symbol('s2');
3-8. object 对象类型
object 类型表示非原始类型的类型。
在实际项目开发中,基本用不上。
let a: object = { name: 'CS阿吉' };
3-9. array(数组类型)
数组合并了相同类型的对象,如果想合并不同类型的对象,需要使用联合类型定义。而元组天然支持合并不同类型的对象,但元组长度有限,并且为每个元素定义了类型。
数组类型 的约束定义可使用 字面量[]
和 泛型<>
两种方式。两种定义方式没有本质区别,但我推荐使用 字面量[]
。好处有二: 1. 避免和JSX语法冲突 2. 减少代码量。
// 数组类型
const PEOPLE: string[] = ['阿吉', 'CS阿吉', '勇敢牛牛'];
const STATUS_CODE: Array<number> = [200, 301, 302, 304, 404, 500];
// 元组类型
let tomInfoRecord: [string, number] = ['Tom', 25];
如果明确定义了数组元素的类型,所有不符合类型约定的操作都会报错。
const PEOPLE: string[] = ['阿吉', 'CS阿吉', '勇敢牛牛'];
PEOPLE[0] = '0号阿吉'; // success
PEOPLE[3] = '不怕困难'; // add elements
console.log(PEOPLE); // [ '0号阿吉', 'CS阿吉', '勇敢牛牛', '不怕困难' ]
PEOPLE[0] = 500; // error TS2322: Type 'number' is not assignable to type 'string'.
如果数组中的元素有多个类型,你可以使用联合类型,此时你必须使用 泛型<>
定义类型。
const PEOPLE: Array<number | string> = ['阿吉', 'CS阿吉', '勇敢牛牛'];
PEOPLE[0] = '0号阿吉'; // success
PEOPLE[0] = 500; // success
3-10. tuple(元组类型)
在 JavaScript 中没有元组的概念,因为JS是动态语言,动态语言天然支持多类型的元素数组。
元组类型 限制数组元素的个数和类型,常用于多值返回,具体实践比如 React Hook 中的 useState,如下:
import { useState } from 'react';
function getNumber() {
const [count, setCount] = useState(0);
return [count, '0x1234234'];
}
// x 和 y 是两个不同的元组
let x: [number, string] = [1, 'cs'];
let y: [string, number] = ['cs', 1];
3-11.enum枚举类型
在 JavaScript 中没有枚举的概念,枚举类型 也是TS特有的。enum
被编译为一个双向Map结构。
枚举类型 用于定义包含命名的常量集合,TS支持数字和字符串两种常量值常量成员和。枚举的高级应用,比如异构类型、联合枚举等等,后续再讲解。
枚举类型 定义包含了值和类型。
数字枚举 默认从下标 0 开始,依次递增。也可以设置枚举成员的初始值。
字符串枚举 支持使用字符串初始化枚举成员,不支持成员自增长,每个枚举成员必须初始化。
enum ErrcodeType {
NOLOGIN_ERROR = -1,
FETCHCS_ERROR = -2,
}
enum ErrMsge {
NOLOGIN_MSG = '未登陆',
FETCHCS_MSG = '授权未通过',
}
function isLogin(): { errcode: ErrcodeType, errormsg: ErrMsge } {
// ajax
return { errcode: ErrcodeType.FETCHCS_ERROR, errormsg: ErrMsge.FETCHCS_MSG }
}
console.log(isLogin()); // { errcode: -2, errormsg: '授权未通过' }
数字枚举 具备自映射,字符串枚举不具备。
enum ErrcodeType {
NOLOGIN_ERROR = -1,
FETCHCS_ERROR = -2,
}
enum ErrMsge {
NOLOGIN_MSG = '未登陆',
FETCHCS_MSG = '授权未通过',
}
console.log(ErrcodeType.NOLOGIN_ERROR); // -1
console.log(ErrcodeType[-1]); // NOLOGIN_ERROR
console.log(ErrMsge.NOLOGIN_MSG); // 未登陆
console.log(ErrcodeType['未登陆']); // undefined
// 「编译阶段不会报错,但ts文件中鼠标移动到'
枚举仅和自身兼容,在项目开发中,建议将“相同“的枚举类型抽离为公共枚举。
enum ErrcodeType {
NOLOGIN_ERROR = -1,
FETCHCS_ERROR = -2,
}
enum ErrCode {
NOLOGIN_ERROR = -1,
FETCHCS_ERROR = -2,
}
console.log(ErrcodeType.NOLOGIN_ERROR == ErrCode.NOLOGIN_ERROR);
// error TS2367: This condition will always return 'false' since the types 'ErrcodeType' and 'ErrCode' have no overlap.
// 意思是虽然两个枚举数值都是 -1 ,但在TS中这两个枚举值恒不相等
让我们扩展延伸一下,学JS时,有一句经典的话 闭包在JS中无处不在
,枚举类型 编译后的产物就是闭包。如下代码示例:
// 1.ts文件
enum ErrcodeType {
NOLOGIN_ERROR = -1,
FETCHCS_ERROR = -2,
}
enum ErrMsge {
NOLOGIN_MSG = '未登陆',
FETCHCS_MSG = '授权未通过',
}
// tsc 1.ts
// 1.js
var ErrcodeType;
(function (ErrcodeType) {
ErrcodeType[ErrcodeType["NOLOGIN_ERROR"] = -1] = "NOLOGIN_ERROR";
ErrcodeType[ErrcodeType["FETCHCS_ERROR"] = -2] = "FETCHCS_ERROR";
})(ErrcodeType || (ErrcodeType = {}));
var ErrMsge;
(function (ErrMsge) {
ErrMsge["NOLOGIN_MSG"] = "\u672A\u767B\u9646";
ErrMsge["FETCHCS_MSG"] = "\u6388\u6743\u672A\u901A\u8FC7";
})(ErrMsge || (ErrMsge = {}));
// 让我们打印观察一下 ErrcodeType ErrMsge
console.log(ErrcodeType);
// {
// NOLOGIN_ERROR: -1,
// '-1': 'NOLOGIN_ERROR',
// FETCHCS_ERROR: -2,
// '-2': 'FETCHCS_ERROR'
// }
console.log(ErrMsge);
// { NOLOGIN_MSG: '未登陆', FETCHCS_MSG: '授权未通过' }
3-12. void 类型
void类型 表示没有任何返回值。
它仅适用于没有返回值的函数。
void类型的变量只能赋值给 any类型 和 unkown类型。
3-13. never 类型
never 表示永远不会存在的值
的类型。让我们用一段有趣的代码,来看这个定义。
type Never = 1 & 2; // 等价于 type Never = never
你是不是以为Never
意味着,既能是1又能是2,显然JS中不存在这样的值。所以never
意味着什么值都不兼容
。而 any
意味着 什么值都能兼容
。any 和 never 含义相反。
never 是任何类型的子类型,即 never 可以给所有类型赋值,但 除了never自身的所有类型 都不能赋值给 never类型。
never 仅和自身类型兼容。NaN 连自身都不相等。
基于上述特性,never可用于禁止接口下特定的属性,即修饰为只读属性。如下:
interface ItemProps {
name: string;
description?: never;
}
function makeItem(props: ItemProps): void {
const { name, description } = props;
props.description = name + description; // error TS2322: Type 'string' is not assignable to type 'never'.
}
makeItem({ name: 'CS阿吉', description: ' is good item!' });
在上述代码中,给props.description
赋值任何类型都会提示类型错误,相当于description
是只读属性。
我们通常利用 「x | never
的值等于x
」 这个特性,来过滤对象中的字段。比如
// 任意函数类型
type AnyFunc = (...args: any[]) => any;
// 任意对象类型
type AnyObj = { [key: string]: any }
// 删除函数类型的属性
type GetStaticKeysFor<T> = {
[K in keyof T]: T[K] extends AnyFunc ? never : K
}[keyof T]; // keyof T => a | never | never => a
type GetStaticFor<T> = {
[K in GetStaticKeysFor<T>]: T[K]
}
// 保留函数类型的属性
type GetFuncKeysFor<T> = {
[K in keyof T]: T[K] extends AnyFunc ? K : never
}[keyof T];
type GetFuncFor<T> = {
[K in GetFuncKeysFor<T>]: T[K]
}
type ABC = { a: 1, b(): string, c(): number };
// 测试用例
type DeleteFnExm = GetStaticFor<ABC>;
// type DeleteFnExm = {
// a: 1;
// }
type KeepFnExm = GetFuncFor<ABC>;
// type KeepFnExm = {
// b: () => string;
// c: () => number;
// }
never类型 通常用于定义一个统一抛出错误的函数。如下:
function ThrowErrorMsg(msg: string): never {
throw Error(msg);
}
3-14. unknown 类型
unknown类型 用于描述类型不确定的变量。含义是 类型可能为任何一种类型。这意味着,使用 unkown 之前需要使用断言才能正常使用。
// err.ts
function addUnknown(a: unknown, b: unknown) {
return a + b;
}
// tsc err.ts
// error TS2365: Operator '+' cannot be applied to types 'unknown' and 'unknown'.
// success.ts
function addUnknown(a: unknown, b: unknown) {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
}
// tsc success.ts
// 编译成功,未报错
我们可以对 any类型 的值进行任意操作,无需在编译阶段进行类型检查。TS 3.0 引入的 unknown类型 是 any类型 的安全类型,对 unknown类型 的值进行操作之前,需要检查其类型,因为 unknown类型 只能赋值给 any类型 和 unknown类型。
let can_not_known: unknown;
let any_thing: any = can_not_known; // success
let not_known: unknown = can_not_known; // success
let num: number = can_not_known; // error TS2322: Type 'unknown' is not assignable to type 'number'.
所有类型收窄的手段对 unknown 都有效,如果不进行类型收窄,对 unknown类型 的.操作
都会报错。
let result: unknown;
if (typeof result === 'number') {
result.toFixed(); // 鼠标移动到result会提示:let result: number
}
result.toFixed(); // error TS2339: Property 'toFixed' does not exist on type 'unknown'.
4. 对象类型
4-1. 接口类型与类型别名
开发中,我们通常用 interface
和 type
声明来约束对象,也可用来约束函数,仅仅是语法不同。
我们通常把 interface 称为接口,把 type 称为类型别名。
interface 主要用于定义数据模型,type 主要用于类型别名。
interface BrandPavilion {
name: string; // 品牌馆名称
score: number; // 权重
}
interface SetBrandPavilion {
(name: string, score: number): void;
}
type BrandPavi = {
name: string; // 品牌馆名称
score: number; // 权重
};
type SetBrandPavi = (name: string, score: number) => void;
4-2. interface接口类型
接口是一系列抽象方法的声明。接口的作用是对值的结构类型进行命名
和定义数据模型
。
接口类型、类型别名可将内联类型抽离,实现类型复用。在开发中,抽离到公共文件通过 export
导出即可。
接口具备可选属性、只读属性、多余属性检查,代码示例如下:
interface Person {
name: string;
readonly age: number; // 可选属性
hobby?: string; // 只读属性
}
// 多余属性检查 sex
let p: Person = { name: '阿吉', age: 10, sex: '男' };
// error TS2322: Type '{ name: string; age: number; sex: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'sex' does not exist in type 'Person'.
绕开多余属性检查的方案就是使用断言as
。如下:
let p: Person = { name: '阿吉', age: 10, sex: '男' } as Person; // success
还有一种特殊的方式绕开检查:变量赋值已知对象
。这种方式通常用于解决函数传参时形参和实参不一致。
interface Person {
name: string;
readonly age: number; // 可选属性
hobby?: string; // 只读属性
}
let clone_p = { name: '阿吉', age: 10, sex: '男' };
let p: Person = clone_p;
function getInfo(person: Person) {
console.log(`${person.name} is ${person.age} years old!`);
}
getInfo(clone_p); // success
getInfo({ name: '阿吉', age: 10, sex: '男' }); // error
// error TS2345: Argument of type '{ name: string; age: number; sex: string; }' is not assignable to parameter of type 'Person'.
// Object literal may only specify known properties, and 'sex' does not exist in type 'Person'.
interface
还可以定义构造函数的类型,但在实际开发中,关于构造函数类型基本没有场景需要去定义,更多的是通过typeof去获取。
export interface Person {
name: string;
new (name: string): Person;
}
const P: Person;
new P('GGG'); // success
new P(123); // error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
4-3. type类型别名
类型别名的本质是给类型取名一个新名字,而不是创建一个新类型。
type NameType = string;
type AgeType = number;
type SayHhhFun = () => string;
interface Person {
name: NameType;
age: AgeType;
sayHhh: SayHhhFun;
}
let person: Person = { name: 'CS阿吉', age: 18, sayHhh: () => 'Hhh' }; // success
接口类型、类型别名
可将内联类型抽离,通过类型抽象
实现类型复用。在开发中,抽离到公共文件通过 export
导出即可。
// GItemListProps 和 GItemMenuProps 相关联
type GItemListProps = { // 商品列表
id: string;
name: string;
onShowList: () => void;
};
type GItemMenuProps = { // 商品菜单
id: string;
name: string;
onReportClick: () => void;
onSetItem: () => void;
};
// 抽象可以减少重复,类型抽象可以实现类型复用
interface BrandCard {
id: string;
name: string;
}
type GItemListProps = BrandCard & {
onShowList: () => void;
};
type GItemMenuProps = BrandCard & {
onReportClick: () => void;
onSetItem: () => void;
};
在开发中,type类型别名
主要用于解决接口类型无法覆盖的场景,比如联合类型、交叉类型等,还可提取接口属性类型、引用自身。
联合类型 通常用
|
运算符连接两个类型(也可多个)。比如A|B
,A或B的约束至少满足一个。
交叉类型 通常用
&
运算符连接两个类型(也可多个)。比如A&B
,A和B的约束同时满足。
扩展类型
A extends B
A继承自B,类似于&
type retcode = string | number; // 联合类型:retcode
interface BrandCard { // 类型复用BrandCard,GItemProps实现了动态生成类型
id: string;
name: string;
}
// 交叉类型:GItemProps
type GItemProps = BrandCard & {
onReportClick: () => void;
onShow: () => void;
};
// 提取接口属性类型
type BrandNameType = BrandCard['name']; // BrandNameType 是 string类型 的别名
type DOMCalcMethod = () => ();
// 引用自身:DOMAccessTree、TreeNode
interface DOMAccessTree { // DOMAccessTree 是一种递归的数据结构
[key: string]: DOMAccessTree | DOMCalcMethod;
}
type TreeNode<T> = {
value: T; // 类型别名也可是泛型
leftNode: TreeNode<T>;
rightNode: TreeNode<T>;
}
type 进阶,让我们看如何删除/保留一个对象类型的函数项
// 定义函数
type AnyFunc = (...args: any[]) => any;
// 删除函数类型的属性
type GetStaticKeysFor<T> = {
[K in keyof T]: T[K] extends AnyFunc ? never : K
}[keyof T];
type GetStaticFor<T> = {
[K in GetStaticKeysFor<T>]: T[K]
}
// 保留函数类型的属性
type GetFuncKeysFor<T> = {
[K in keyof T]: T[K] extends AnyFunc ? K : never
}[keyof T];
type GetFuncFor<T> = {
[K in GetFuncKeysFor<T>]: T[K]
}
type ABC = { a: 1, b(): string, c(): number };
// 测试用例
type DeleteFnExm = GetStaticFor<ABC>;
// type DeleteFnExm = {
// a: 1;
// }
type KeepFnExm = GetFuncFor<ABC>;
// type KeepFnExm = {
// b: () => string;
// c: () => number;
// }
同时,type 可以搭配 typeof
使用。typeof
可获取变量、对象、函数的类型。
interface Person {
name: string;
age: number;
}
const str: string = 'CS';
type StrType = typeof str; // 等价于 type StrType = string;
const p: Person = { name: 'CS阿吉', age: 18 };
type PType = typeof p; // 等价于 type PType = Person;
// typeof 也可获取 嵌套对象 的类型
function getNum(x: string): number {
return Number(x);
}
type Func = typeof getNum; // 等价于 type Func = (x: string) => number
扩展延伸 keyof:typeof
操获取一个变量或对象或函数的类型。 keyof
获取某类型的所有键的类型集合,其返回类型是联合类型。
在项目中实战,抽象出示例代码,如下:
interface Person {
name: string;
age: number;
}
const person: Person = {
name: 'CS阿吉',
age: 18,
};
// 需要实现一个函数,修改 person 对象中属性的值
const changePersonQuestion = (key, value) => {
person[key] = value;
};
// 问题:如何定义 key、value 的类型?
// 解决方案如下:
const changePersonAnswer = <T extends keyof Person>(key: T, value: Person[T]): void => {
person[key] = value;
}
// 延伸:上述代码不符合纯函数,有隐藏依赖关系。将person作为形参传入,可变为纯函数,无副作用。
4-4. interface VS type
关于二者的区别,官方文档解释如下:
•
1.An interface can be named in an extends or implements clause, but a type alias for an object type literal cannot.
•
1.An interface can have multiple merged declarations, but a type alias for an object type literal cannot.
1.可以在 extends 或 implements 子句中命名接口,但不能在对象类型文字的类型别名中命名。
1.一个接口可以有多个合并声明,但对象类型字面量的类型别名不能。
说人话总结一下就是:
interface
:interface 遇到同名的 interface、class 会自动聚合;interface 仅能声明 object、function 类型
type
:type 不可重复声明;type 声明不受限;
同时,两者都允许扩展extends,只是语法不同。
interface PersonA {
name: string;
}
interface PersonA {
age: number;
}
let p: PersonA = { name: 'CS阿吉', age: 18 }; // 同名interface自动聚合
type PersonB = { name: string };
/** type不可重复声明 error TS2300: Duplicate identifier 'PersonB'. */
// type PersonB = { age: number };
// interface extends interface
interface PersonC {
name: string;
};
interface PersonCC extends PersonC {
age: number;
}
// type extends type
type PersonD = {
name: string;
};
type PersonDD = PersonD & { age: number }
// interface extends type
type PersonE = {
name: string;
};
interface PersonEE extends PersonE {
age: number;
};
// type extends interface
interface PersonF {
name: string;
};
type PersonFF = PersonF & { age: number }
5. 函数类型
5-1.定义、可缺省、is
通常用 type
来定义一个函数类型。函数的返回值类型可被推断出来,即可缺省。函数没有返回值时可用void
。
函数返回值类型推断 + 泛型 ==> 复杂的类型推断
type AddFuncType = (a: number, b: number) => number; // TypeScript 函数类型定义
const add: AddFuncType = (a, b) => a + b;
const addMissing = (a: number, b: number) => a + b; // const add: (a: number, b: number) => number
interface Person {
say: () => void;
eat: (a: string) => void;
}
const person: Person = {
say: () => console.log('hello'),
eat: (fruit: string) => console.log(`eat a ${fruit}`),
}
ES6中的Generator函数在TS中也提供了Generator来约束。
type AnyYieldType<T> = T;
type AnyReturnType<T> = T;
type AnyNextType = number;
function* HWG():
Generator<AnyYieldType<string>, AnyReturnType<boolean>, AnyNextType>
{
const resVal = yield 'Hello'; // const resVal: number
console.log(resVal);
yield 'World';
return true;
}
函数参数也可以用类型谓词is
来约束,代码如下:
类型谓词函数通常用于类型守卫。类型守卫出现的原因是因为TS要求必须明确对象的属性是否存在,获取不存在的属性得到undefined不符合预期。
function isString(s: any): s is string { // 如果返回true,代表s是string类型
return typeof s === 'string';
}
let test = '';
if (isString(test)) {
console.log('this is string type');
}
我们可以抽象出一个类型守卫函数judgeType
。
本质:当类型守卫返回true时,将被守卫的类型收窄到is指定的类型。
function judgeType<T>(Target: any, TargetProperty: keyof T):Target is T {
return (Target as T)[TargetProperty] !== undefined;
}
class Animal {}
class TeacupDog extends Animal {
constructor(){
super();
}
wang() {
console.log('TeacupDog wang');
}
}
class GoldenDog extends Animal {
constructor(){
super();
}
wang() {
console.log('GoldenDog wang');
}
}
class Cat extends Animal {
constructor(){
super();
}
miao() {
console.log('Cat miao');
}
}
const teacupDog = new TeacupDog(); // 茶杯犬
const goldenDog = new GoldenDog(); // 金毛
const cat = new Cat(); // 猫咪
if (judgeType(teacupDog, 'wang')) {
teacupDog.wang();
}
if (judgeType(cat, 'miao')) {
cat.miao();
}
5-2.可选参数、默认参数
函数定义时搭配?
可传入可选参数,可选参数一定要放在参数的最后。TS也会根据默认参数的类型来推断函数参数的类型。
函数的默认参数类型必须是参数类型的子类型。
function operate(a: number | string, b = 2, c?: string) { // (parameter) b: number
if (c === 'add') {
return Number(a) + b;
} else {
return Number(a) - b;
}
}
5-3.剩余参数
TS也支持剩余参数的类型定义。
function add(...arg: (number | string)[]): number {
return arg.reduce<number>((a, b) => a + Number(b), 0);
}
add(0, '1', 2); // 3
5-4.this
TS可以在函数第一个参数中声明 this 指代的对象。
TS转译为JS后,伪形参this会被抹掉。
function sayName(this: Window) {
console.log(this.name);
}
window.sayName = sayName;
const person = {
name: 'person的name',
sayName
};
window.sayName();
person.sayName(); // error TS2684: The 'this' context of type '{ name: string; sayName: (this: Window) => void; }' is not assignable to method's 'this' of type 'Window'.
我们同样可以显式约束class中的this类型。示例代码如下:
interface Horn { // 扬声器
amplifyVolume(sound: (this: void) => void): void; // 放大声音
}
class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
say(this: Person) {
console.log('hello');
}
// 开始链式风格书写,可以把Person当作一个链式调用库
eat(): this { // 用this可 简洁表达返回值类型,而不用写为Person
console.log(`${this.name} eat dinner`);
return this;
}
sleep(): this {
console.log(`${this.name} Slept for a day`);
return this;
}
wakeup(): Person {
console.log(`${this.name} wake up`);
return this;
}
}
const person = new Person('阿吉');
const horn: Horn = {
amplifyVolume(fn) {
fn();
}
}
person.eat() // 阿吉 eat dinner
.sleep() // 阿吉 Slept for a day
.wakeup() // 阿吉 wake up
.say(); // hello
horn.amplifyVolume(person.say); // error TS2345: Argument of type '(this: Person) => void' is not assignable to parameter of type '(this: void) => void'.
5-5.函数重载
多态:同一行为在不同对象上效果不同。
JS是动态语言,天然支持多态。
重载:多个同名函数的参数类型不同。
JS中不存在重载。JS不对传参类型进行约束,对于同名函数,后声明的函数会覆盖先声明的。
在 TypeScript 中,函数重载列表中的每个定义必须是函数实现的子集。TypeScript 从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义。
interface A {
num: number;
}
interface B extends A {
count: string;
}
// 函数重载列表 顺序: AB
function transform(a: A): string;
function transform(a: B): number;
// 函数实现
function transform(a: A | B): any {}
// 测试用例
let a = { num: 1 };
let b = { num: 1, count: '' };
// a只找A类型。b可以找A或B类型,当前函数重新载列表中第一个匹配的是A,所以下面两个都是string。
let str = transform(a as A); // string
let num = transform(b as B); // string
/**
* 如果 函数重载列表顺序 换成 BA
* function transform(a: B): number;
* function transform(a: A): string;
* 则 num 返回值类型会变化
* let str = transform(a as A); // string
* let num = transform(b as B); // number
*/
6. 字面量类型
6-1. 定义
在 TS 中,用字面量表示类型就是字面量类型。
三种分类:字符串字面量类型、数字字面量类型、布尔字面量类型。下面分别介绍一下各自的用途。
let strLiteralType: 'string' = 'string'; // let strLiteralType: "string"
let numLiteralType: 1 = 1; // let numLiteralType: 1
let boolLiteralType: true = true; // let boolLiteralType: true
字符串字面量 通常用多个来组成联合类型,用以描述明确成员的类型。数字字面量、布尔字面量用途同上。
在const
定义常量的类型缺省时,其类型为赋值的字面量类型。
在let
定义常量的类型缺省时,其类型为赋值字面量类型的父类型。在TS中,字面量子类型转为父类型的设计叫做Literal Widening
(字面量类型拓宽)。同理,也存在Type Widening
(类型拓宽)、Type Narrowing
(类型收窄)。
type OType = 'add' | 'sub';
function operation(a: number, b: number, otype: OType): number {
if (otype === 'add') {
return a + b;
} else if (otype === 'sub') {
return a - b;
} else {
return 0;
}
}
operation(2, 4, 'add'); // success
operation(2, 4, 'ADD'); // error TS2345: Argument of type '"ADD"' is not assignable to parameter of type 'OType'.
// const 赋值的字面量类型
const strC = 'strC'; // const strC: "strC"
const boolC = false; // const boolC: false
const numC = 1; // const numC: 1
// let 赋值字面量类型的父类型
// literal widening:字面量类型扩宽
let strCC = 'strCC'; // let strCC: string
let boolCC = false; // let boolCC: boolean
let numCC = 1; // let numCC: number
6-2.Literal Widening
定义:用let
、var
声明的变量、函数形参、对象可写属性
没有显式声明类型,而是通过类型推导出其类型是字面量类型,则其会自动拓宽到父类型。这种设计就叫做 Literal Widening 。
对象属性可用
as const
进行类型收窄,将该属性变成只读类型。
let strCC = 'strCC'; // let strCC: string
// 变量 strCC 的类型是值为 strCC 的字面量类型,变量 strCC 的类型被拓宽到 string
function add(a = 1, b = 2): number {
return a + b;
} // function add(a?: number, b?: number): number
// 变量 a 的类型是值为 1 的字面量类型,变量 a 的类型被拓宽到 number
let obj = {
name: 'CS阿吉', // (property) name: string
age: 18 as const, // (property) age: 18
};
// 对比
const strCCC = 'strCCC'; // const strCCC: "strCCC"
let a = strCCC; // let a: string
const strCCCC: 'strCCCC' = 'strCCCC'; // const strCCCC: "strCCCC"
let b = strCCCC; // let b: "strCCCC"
6-3.Type Widening
定义:用let
、var
声明的变量
没有显式声明类型,并且该变量的值是null
或undefined
,则其类型拓宽为any。这种设计就叫做 Type Widening 。
let x; // let x: any
let y = null; // let y: any
let z = undefined; // let z: any
// 对比
const a = null; // const a: null
let b = a; // let b: null 哪怕用let声明,其类型也为null,而非any。因为a的类型是null。
function add (a = null) { // (parameter) a: null
return a;
}
6-4.Type Narrowing
定义:将变量的类型由宽泛集合缩小到较小集合。
该特性最常用于类型守卫
。
// example 1
function getSomething(a: number | string | null): number {
if (typeof a === 'number') {
return a; // (parameter) a: number
} else if (typeof a === 'string') {
return Number(a); // (parameter) a: string
} else {
return Number(a); // (parameter) a: null
}
}
// example 2
let result: unknown;
if (typeof result === 'number') {
result.toFixed(); // 鼠标移动到result会提示:let result: number
}
// example 3
type SrtType = string | 'str'; // type SrtType = string
通过& {}
可以避免类型收窄。
type Str = 's' | 'str'; // type Str = "s" | "str"
type StrTypeSmall = 's' | 'str' | string ; // type StrType = string
type StrType = 's' | 'str' | string & {}; // type StrType = "str" | (string & {})
6-5.总结
对于声明的简单类型字面量,let拓宽类型,const收窄类型。
对于声明的对象(无论let声明还是const声明),其属性会自动推断类型,可用as const 收窄,将其变为只读类型。
// 简单类型字面量
let a = 1; // let a: number
const b = 1; // const b: 1
// 对象
let obj = {
name: 'CS阿吉', // (property) name: string
age: 18 as const, // (property) age: 18
};
7. 类类型
7-1.extends
子类继承父类,如果子类中有constructor
,则必须在其中调用super
方法。
super会调用父类构造函数方法。
class Animal {}
class Dog extends Animal{}
class Snake extends Animal{
constructor() {
super();
}
}
class Cat extends Animal{
constructor() {} // error TS2377: Constructors for derived classes must contain a 'super' call.
}
7-2.修饰符
public: 公有属性或方法(可被任意访问、调用)。没有修饰符时,默认为public。
private: 私有属性或方法(仅可被同一类访问、调用)
protected: 受保护属性或方法(仅可被自身及子类访问、调用)
readonly: 只读修饰符,不可被改变。
class Father {
public name: string;
public readonly idcard = 'father';
private address: string = 'dad';
protected secret: string = 'as fater, I also like playing games';
constructor(name: string) {
this.name = name;
this.idcard;
this.address;
this.secret;
}
callme() {
console.log(`I am father, call me ${this.address} or ${this.idcard}`);
}
tellSecret() {
console.log(this.secret);
}
}
class Son extends Father {
static displayName = '类自身的属性';
constructor(name: string) {
super(name);
}
getFartherSecret() {
console.log(this.secret);
}
}
let father = new Father('阿吉');
let son = new Son('小小阿吉');
console.log(father.name);
console.log(son.name);
father.name = '父亲';
console.log(father.name);
son.getFartherSecret();
console.log(Son.displayName);
father.callme();
father.idcard = '父亲'; // error TS2540: Cannot assign to 'idcard' because it is a read-only property.
上述代码提到了static
静态属性,同样的也有static
静态方法。没有static
修饰的属性和方法,只有类在实例化时才会被初始化。static
静态属性和方法只存在于类上,而不是类的实例上。我们可以直接通过类来访问静态属性和静态方法。
static
静态属性用途:共享属性,提高性能。抽离出和类相关的常量、不依赖this的属性和方法,将其变为static
属性和方法。
若静态方法中依赖this,则必须显式注解this类型。非静态方法中的this默认指向类实例。
7-3.抽象类
抽象类:不能被实例化,仅能被子类继承的类。
注:在声明类的时候,同时声明了名为类名的接口类型。除了constructor构造函数以外的成员,都是该接口类型的成员。显示注解一个变量的类型为该类,若缺少了这些成员会报错。
抽象类的实现方式有两种:(1)定义抽象类,class搭配abstract关键字约束 (2)使用接口,interface搭配implements关键字约束。
抽象类不仅能定义类成员的类型,还能共享非抽象属性和方法;接口只能定义类成员的类型。
继承抽象类的子类必须实现抽象属性和方法,不然会报错。
这里提到抽象,可以看一下以前的一篇文章 D 004 抽象工厂模式(Abstract Factory)
下面先看使用抽象类定义
abstract class Parent {
abstract name: string;
abstract sonName: string;
abstract getSon(): string;
eat(): void {
console.log('Father eat');
}
sayHello(): void {
console.log('Father say hello');
}
}
/**
* 子类在继承 Parent 时,
* 必须实现 name、sonName、getSon(),
* 否则会报错。
* Father中的eat()、sayHello(),
* 如果子类实例中没定义,则直接获取Fater中的非抽象方法。
*/
class Dad extends Parent {
name: string;
sonName: string;
constructor(name: string, sonName: string) {
super();
this.name = name;
this.sonName = sonName;
}
getSon(): string {
return this.sonName;
}
sayHello(): void {
console.log('dad say hello');
}
}
const dad = new Dad('阿吉', '小小阿吉');
console.log(dad.getSon()); // 小小阿吉
dad.sayHello(); // dad say hello
dad.eat(); // Father eat
下面再看使用接口定义
interface Parent {
name: string;
sonName: string;
getSon:() => string;
}
/**
* 子类在继承 Parent 时,
* 必须实现 name、sonName、getSon(),
* 否则会报错。
* Father中的eat()、sayHello(),
* 如果子类实例中没定义,则直接获取Fater中的非抽象方法。
*/
class Dad implements Parent {
name: string;
sonName: string;
constructor(name: string, sonName: string) {
this.name = name;
this.sonName = sonName;
}
getSon(): string {
return this.sonName;
}
sayHello(): void {
console.log('dad say hello');
}
}
const dad = new Dad('阿吉', '小小阿吉');
console.log(dad.getSon()); // 小小阿吉
dad.sayHello(); // dad say hello
// dad.eat(); 接口不能共享属性和方法,仅仅能定义类型
8. 联合类型
定义:其类型是多个原子类型中的一个,连接多个原子类型使用|
。
function getSomething(a: number | string | null): number {
if (typeof a === 'number') {
return a; // (parameter) a: number
} else if (typeof a === 'string') {
return Number(a); // (parameter) a: string
} else {
return Number(a); // (parameter) a: null
}
}
接下来复习一个有趣的知识点,类型收窄。但同样的,我们有手段& {}
来控制类型收窄。
type Str = 's' | 'str'; // type Str = "s" | "str"
type StrTypeSmall = 's' | 'str' | string ; // type StrType = string
type StrType = 's' | 'str' | string & {}; // type StrType = "str" | (string & {})
9. 交叉类型
定义:将多个类型合并成一个类型,连接多个类型使用&
。
type PersonD = {
name: string;
};
type PersonDD = PersonD & { age: number }
10. 泛型
定义:将类型参数化,然后进行某种特定的处理。
枚举类型不支持泛型
interface Person {
name: string;
age: number;
}
const person: Person = {
name: 'CS阿吉',
age: 18,
};
// 需要实现一个函数,修改 person 对象中属性的值
const changePersonQuestion = (key, value) => {
person[key] = value;
};
// 问题:如何定义 key、value 的类型?
// 解决方案如下:
const changePersonAnswer = <T extends keyof Person>(key: T, value: Person[T]): void => {
person[key] = value;
}
条件分配类型:在条件类型判断的情况下,如果入参是联合类型,则会被拆解成一个个的原子类型进行类型运算。
简单说就是,泛型 + extends + 三元运算符 = 条件分配
type StrNumType = string | number;
type StrNumJudgment<T> = T extends StrNumType? T : null;
type ComeInType = string | boolean;
type OutType = StrNumJudgment<ComeInType>; // type OutType = string | null
type OutWithoutGeneric = ComeInType extends StrNumType ? ComeInType : null; // type OutWithoutGeneric = null
// OutType 触发条件判断,OutWithoutGeneric 没有触发条件判断