公众号:CS阿吉

TS 001 初探TypeScript类型_typescript

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 没有触发条件判断