1. 基础类型

TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

1.1.boolean

最基本的数据类型就是简单的 true/false 值,在JavaScript 和 TypeScript 里叫做 boolean(其它语言中也一样)。

1
2
3
let isDone: boolean = false;
isDone = true;
// isDone = 2 // error

1.2.number

和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015中引入的二进制和八进制字面量。

1
2
3
4
let a1: number = 10 // 十进制
let a2: number = 0b1010 // 二进制
let a3: number = 0o12 // 八进制
let a4: number = 0xa // 十六进制

1.3.string

JavaScript 程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string 表示文本数据类型。 和 JavaScript 一样,可以使用双引号(")或单引号(')表示字符串。

1
2
3
4
5
let name:string = 'tom'
name = 'jack'
// name = 12 // error
let age:number = 12
const info = `My name is ${name}, I am ${age} years old!`

1.4.undefined 与 null

在 JavaScript 中,undefined 和 null 是两个基本数据类型

在TypeScript中,它们各自的类型也是undefined和null,也就意味着它们既是实际的值,也是自己的类型

1
2
let u: undefined = undefined
let n: null = null

1.5.Array

TypeScript 像 JavaScript 一样可以操作数组元素

  • 定义单一数据类型
1
2
3
4
5
// 1.在元素类型后面接上[]
const list1: number[] = [1, 2, 3]

// 2.使用数组泛型,Array<元素类型>
const list2: Array<number> = [1, 2, 3]
  • 定义多种数据类型
1
2
const list3: (string | number | boolean)[] = [123, true, "ts"]
const list4: Array<string | number | boolean> = [true, "ts", 456]

1.6.Tuple

元组类型允许表示一个已知元素数量类型的数组,各元素的类型不必相同

比如,可以定义一对值分别为 stringnumber 类型的元组。

1
2
3
let t1: [string, number]
t1 = ['hello', 10] // OK
t1 = [10, 'hello'] // Error

当访问一个已知索引的元素,会得到正确的类型:

1
2
console.log(t1[0].substring(1)) // OK
console.log(t1[1].substring(1)) // Error, 'number' 不存在 'substring' 方法

元组和数组的区别

  • 首先,数组中通常建议存放相同类型的元素,不同类型的元素是不推荐放在数组中。(可以放在对象或者元组中)
  • 其次,元组中每个元素都有自己特性的类型,根据索引值获取到的值可以确定对应的类型
1
2
3
4
5
const info: (string | number)[] = ["aaa", 18, 1.8]
const item1 = info[0] // 不能确定类型

const tInfo: [string, number, number] = ["aaa", 18, 1.8]
const item2 = tInfo[0] // 一定是string类型

应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在函数中使用元组类型是最多的(函数的返回值)
function useState(initialState: number): [number, (newValue: number) => void] {
let stateValue = initialState
function setValue(newValue: number) {
stateValue = newValue
}

return [stateValue, setValue]
}

const [count, setCount] = useState(10)
console.log(count)
setCount(100)

1.7.enum

enum 类型是对 JavaScript 标准数据类型的一个补充。 使用枚举类型可以为一组数值赋予友好的名字

1
2
3
4
5
6
7
8
9
10
enum Color {
Red,
Green,
Blue
}

// 枚举数值默认从0开始依次递增
// 根据特定的名称得到对应的枚举数值
const myColor: Color = Color.Green // 0
console.log(myColor, Color.Red, Color.Blue)

默认情况下,从 0 开始为元素编号。 也可以手动的指定成员的数值。 例如,将上面的例子改成从 1 开始编号:

1
2
3
4
5
6
enum Color {
Red = 1,
Green,
Blue
}
const c: Color = Color.Green

或者,全部都采用手动赋值:

1
2
enum Color {Red = 1, Green = 2, Blue = 4}
const lor = Color.Green

枚举类型可以由枚举的值得到它的名字。 例如,知道数值为 2,但是不确定它映射到 Color 里的哪个名字,可以查找相应的名字

1
2
3
4
enum Color {Red = 1, Green, Blue}
const colorName: string = Color[2]

console.log(colorName) // 'Green'

使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义枚举类型
enum Direction {
LEFT,
RIGHT
}

const d1: Direction = Direction.LEFT

function turnDirection(direction: Direction) {
switch(direction) {
case Direction.LEFT:
console.log("角色向左移动一个格子")
break
case Direction.RIGHT:
console.log("角色向右移动一个格子")
break
}
}

// 监听键盘的点击
turnDirection(Direction.LEFT)

1.8.any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any 类型来标记这些变量:

1
2
3
let notSure: any = 4
notSure = 'maybe a string'
notSure = false // 也可以是个 boolean

在对现有代码进行改写的时候,any 类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。并且当你只知道一部分数据的类型时,any 类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

1
2
3
const list: any[] = [1, true, 'free']

list[1] = 100

1.9.void

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

  • 在TS中如果一个函数没有任何的返回值, 那么返回值的类型就是void类型

  • 如果返回值是void类型, 那么我们也可以返回undefined(TS编译器允许这样做而已), null不可以

1
2
3
4
5
6
function fn(): void {
console.log('fn()')
// return undefined
// return null // error
// return 1 // error
}

声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined

1
let a: void = undefined

1.10.Object

object 表示非原始类型,也就是除 numberstringboolean之外的类型。

使用 object 类型,就可以更好的表示像 Object.create 这样的 API。例如:

1
2
3
4
5
6
7
8
9
function fn2(obj:object):object {
console.log('fn2()', obj)
return {}
// return undefined
// return null
}
console.log(fn2(new String('abc')))
// console.log(fn2('abc') // error
console.log(fn2(String))

1.11.Symbol

在ES5中,是不可以在对象中添加相同的属性名称的,比如下面的做法:

1
2
3
4
const person = {
identity: "aaa",
identity: "bbb" // error
}

通常的做法是定义两个不同的属性名字:比如identity1和identity2

但是也可以通过symbol来定义相同的名称,因为Symbol函数返回的是不同的值

1
2
3
4
5
6
7
const s1: symbol = Symbol("title")
const s2: symbol = Symbol("title")

const person = {
[s1]: "aaa",
[s2]: "bbb"
}

1.12.unkkown

unknown类型默认情况下在上面进行任意的操作都是非法的
要求必须进行类型的校验(缩小), 才能根据缩小之后的类型, 进行对应的操作

1
2
3
4
5
6
let foo: unknown = "aaa"
foo = 123 // error

if (typeof foo === "string") { // 类型缩小
console.log(foo.length, foo.split(" "))
}

1.13.never

  • never 表示永远不会发生值的类型,比如一个函数
  • 如果一个函数中是一个死循环或者抛出一个异常,那么这个函数不会返回东西,那么写void类型或者其他类型作为返回值类型都不合适,就可以使用never类型
  • 实际开发中只有进行类型推导时, 可能会自动推导出来是never类型, 但是很少使用它
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1.一个函数是死循环
function foo(): never {
while(true) {
console.log("-----")
}
throw new Error("123")
}
foo()

// 2.解析歌词的工具,返回值是 never[]
function parseLyric() {
return []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 2. 封装框架/工具库的时候可以使用一下never
// 其他时候在扩展工具的时候, 对于一些没有处理的case, 可以直接报错
function handleMessage(message: string | number | boolean) {
switch (typeof message) {
case "string":
console.log(message.length)
break
case "number":
console.log(message)
break
case "boolean":
console.log(Number(message))
break
default:
const check: never = message
}
}

handleMessage("aaaa")
handleMessage(1234)
handleMessage(true)
handleMessage({ name: "aaa" }) // message 赋值给 check,为never类型

2.进阶类型

2.1.联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种

TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型

使用第一种组合类型的方法:联合类型(Union Type)

  • 联合类型是由两个或者多个其他类型组成的类型
  • 表示可以是这些类型中的任何一个值
  • 联合类型中的每一个类型被称之为联合成员(union’s members)

需求1: 定义一个函数得到一个数字或字符串值的字符串形式值

1
2
3
function toString2(x: number | string) : string {
return x.toString()
}

需求2: 定义一个函数得到一个数字或字符串值的长度

1
2
3
4
5
6
7
8
9
10
function getLength(x: number | string) {

// return x.length // error

if (x.length) { // error
return x.length
} else {
return x.toString().length
}
}

处理多种类型问题

  • 需要使用缩小(narrow)联合
  • TypeScript可以根据我们缩小的代码结构,推断出更加具体的类型
1
2
3
4
5
6
7
function getLength(x: number | string) {
if (typeof x === 'string') {
return x.length
} else {
return x.toString().length
}
}

2.2.类型断言

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构

它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查

类型断言有两种形式:其一是“尖括号”语法;另一个为 as 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 类型断言(Type Assertion): 可以用来手动指定一个值的类型
* 方式一: <类型>值
* 方式二: 值 as 类型 tsx中只能用这种方式
**/

/* 需求: 定义一个函数得到一个字符串或者数值数据的长度 */
function getLength(x: number | string) {
if ((<string>x).length) {
return (x as string).length
} else {
return x.toString().length
}
}
console.log(getLength('abcd'), getLength(1234))
1
2
3
4
5
6
7
8
9
// 类型断言的规则: 断言只能断言成更加具体的类型, 或者 不太具体(any/unknown) 类型
const age: number = 18
// 错误的做法
// const age2 = age as string // error

// TS类型检测来说是正确的, 但是这个代码本身不太正确
const age3 = age as any
const age4 = age3 as string
console.log(age4.split(" "))

2.3.类型推断

类型推断: TS会在没有明确的指定类型的时候推测出一个类型,有下面2种情况:

  • 定义变量时赋值了, 推断为对应的类型
  • 定义变量时没有赋值, 推断为any类型
1
2
3
4
5
6
7
8
/* 定义变量时赋值了, 推断为对应的类型 */
let b9 = 123 // number
// b9 = 'abc' // error

/* 定义变量时没有赋值, 推断为any类型 */
let b10 // any类型
b10 = 123
b10 = 'abc'

2.4.类型别名

以前通过在类型注解中编写 对象类型 和 联合类型,如果想要多次在其他地方使用时,就要编写多次,就可以给对象类型起一个别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 类型别名: type
type MyNumber = number
const age: MyNumber = 18

// 给ID的类型起一个别名
type IDType = number | string

function printID(id: IDType) {
console.log(id)
}

// 打印坐标
type PointType = { x: number, y: number, z?: number }
function printCoordinate(point: PointType) {
console.log(point.x, point.y, point.z)
}

2.5.接口声明

可以通过interface可以用来声明一个对象类型,此处为了和type声明作对比,简要说明基本用法,详细见接口章节

1
2
3
4
5
6
7
8
9
10
11
12
13
type PointType = {
x: number
y: number
z?: number
}

// 接口: interface
// 声明的方式
interface PointType2 {
x: number
y: number
z?: number
}

2.6.类型与接口对比

两者区别

  • 类型别名和接口非常相似,在定义对象类型时,大部分时候,可以任意选择使用
  • 接口的几乎所有特性都可以在 type 中使用(后续会有interface的很多特性)
  • 如果是定义非对象类型,通常推荐使用type,比如DirectionAlignment、一些Function
  • interface 可以重复的对某个接口来定义属性和方法,而type定义的是别名,别名是不能重复的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 1.区别一: type类型使用范围更广, 接口类型只能用来声明对象
type MyNumber = number
type IDType = number | string


// 2.区别二: 在声明对象时, interface可以多次声明
// 2.1. type不允许两个相同名称的别名同时存在
type PointType1 = {
x: number
y: number
}

// type PointType1 = { // error
// z?: number
// }

// 2.2. interface可以多次声明同一个接口名称
interface PointType2 {
x: number
y: number
}

interface PointType2 {
z: number
}

const point: PointType2 = {
x: 100,
y: 200,
z: 300
}

// 3.interface支持继承的
interface IPerson {
name: string
age: number
}

interface IKun extends IPerson {
kouhao: string
}

const ikun1: IKun = {
kouhao: "你干嘛, 哎呦",
name: "kobe",
age: 30
}

// 4.interface可以被类实现(详见TS面向对象)
class Person implements IPerson { }

// 总结: 如果是非对象类型的定义使用type, 如果是对象类型的声明那么使用interface

2.7.交叉类型

另外一种类型合并,就是交叉类型(Intersection Types)

  • 交叉类似表示需要满足多个类型的条件
  • 交叉类型使用 & 符号

交叉类型栗子:

  • 一个交叉类型:表达的含义是numberstring要同时满足

  • 但是有同时满足是一个number又是一个string的值吗?其实是没有的,所以MyType其实是一个never类型

1
type MyType = number & string // MyType: never

在开发中,进行交叉时,通常是对对象类型进行交叉的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 交叉类型: 两种(多种)类型要同时满足
type NewType = number & string // 没有意义

interface IKun {
name: string
age: number
}

interface ICoder {
name: string
coding: () => void
}

type InfoType = IKun & ICoder

const info: InfoType = {
name: "why",
age: 18,
coding: function() {
console.log("coding")
}
}

2.8.非空类型断言

非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义接口
interface IPerson {
name: string
age: number
friend?: {
name: string
}
}

const info: IPerson = {
name: "why",
age: 18
}

// 访问属性: 可选链: ?.
console.log(info.friend?.name)

// 属性赋值:
// 解决方案一: 类型缩小
if (info.friend) {
info.friend.name = "kobe"
}

// 解决方案二: 非空类型断言(有点危险, 只有确保friend一定有值的情况, 才能使用)
info.friend!.name = "james"

2.9.字面量类型

1
2
3
4
5
6
7
// 1.字面量类型的基本上
const name: "why" = "why"
let age: 18 = 18

// 2.将多个字面量类型联合起来 |
type Direction = "left" | "right" | "up" | "down"
const d1: Direction = "left"

2.10.字面量推理

栗子中:对象在进行字面量推理的时候,info其实是一个 {url: string, method: string},所以不能将一个 string 赋值给一个 字面量 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 栗子: 封装请求方法
type MethodType = "get" | "post"
function request(url: string, method: MethodType) {
}

request("http://www.aaa.com", "post")

// TS细节
const info = {
url: "xxxx",
method: "post"
}
// 下面的做法是错误: info.method获取的是string类型
request(info.url, info.method)

// 解决方案一: info.method进行类型断言
request(info.url, info.method as "post")

// 解决方案二: 直接让info对象类型是一个字面量类型
const info2: { url: string, method: "post" } = {
url: "xxxx",
method: "post"
}

// 解决方案三: 将info断言为字面量类型
const info3 = {
url: "xxxx",
method: "post"
} as const
// xxx 本身就是一个string
request(info3.url, info3.method)

2.11.类型缩小

类型缩小的含义

  • 类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄)
  • 可以通过类似于 typeof padding === "number" 的判断语句,来改变TypeScript的执行路径
  • 在给定的执行路径中,可以缩小比声明时更小的类型,这个过程称之为 缩小( Narrowing )
  • 而编写的 typeof padding === "number" 可以称之为 类型保护(type guards)

常见的类型保护有如下几种:

  • typeof
  • 平等缩小(比如===、!==)
  • instanceof
  • in
  • 等等…

2.11.1.typeof

在 TypeScript 中,检查返回的值typeof是一种类型保护,TypeScript 对如何typeof操作不同的值进行编码

1
2
3
4
5
6
7
8
// 1.typeof: 使用的最多
function printID(id: number | string) {
if (typeof id === "string") {
console.log(id.length, id.split(" "))
} else {
console.log(id)
}
}

2.11.2.平等缩小

可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != )

1
2
3
4
5
6
7
8
9
10
11
12
13
// 2.===/!==: 方向的类型判断
type Direction = "left" | "right" | "up" | "down"
function switchDirection(direction: Direction) {
if (direction === "left") {
console.log("左:", "角色向左移动")
} else if (direction === "right") {
console.log("右:", "角色向右移动")
} else if (direction === "up") {
console.log("上:", "角色向上移动")
} else if (direction === "down") {
console.log("下:", "角色向下移动")
}
}

2.11.3.instanceof

JavaScript 有一个运算符instanceof来检查一个值是否是另一个值的“实例”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 3. instanceof: 传入一个日期, 打印日期
function printDate(date: string | Date) {
if (date instanceof Date) {
console.log(date.getTime())
} else {
console.log(date)
}

// if (typeof date === "string") {
// console.log(date)
// } else {
// console.log(date.getTime())
// }
}

2.11.4.in操作符

Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符

  • 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 4.in: 判断是否有某一个属性
interface ISwim {
swim: () => void
}

interface IRun {
run: () => void
}

function move(animal: ISwim | IRun) {
if ("swim" in animal) {
animal.swim()
} else if ("run" in animal) {
animal.run()
}
}

const fish: ISwim = {
swim: function() {}
}

const dog: IRun = {
run: function() {}
}

move(fish)
move(dog)

3. 函数

函数是 JavaScript 应用程序的基础,可以实现抽象层,模拟类,信息隐藏和模块。在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。TypeScript 为 JavaScript 函数添加了额外的功能,可以更容易地使用

3.1.基本示例

和 JavaScript 一样,TypeScript 也可以创建函数。

1
2
3
4
5
6
7
8
9
// 函数声明
function add(x, y) {
return x + y
}

// 函数表达式
const myAdd = function(x, y) {
return x + y
}

3.2.函数类型

3.2.1.定义函数类型

让我们为上面那个函数添加类型:

1
2
3
4
5
6
7
function add(x: number, y: number): number {
return x + y
}

const myAdd = function(x: number, y: number): number {
return x + y
}

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型。

3.2.2.完整函数类型

现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。

1
2
3
4
5
6
7
8
9
10
// 方式一
const myAdd2: (x: number, y: number) => number = function(x: number, y: number): number {
return x + y
}

// 方式二
type addFnType = (x: number, y: number) => number
const myAdd2: addFnTyep = function(x: number, y: number): number {
return x + y
}

3.3.参数相关

3.3.1.可选参数

JavaScript 里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是 undefined。 在TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能

这个时候y的类型其实是 undefinednumber 类型的联合。

1
2
3
4
5
6
7
function add(X: number, y?: number): number {
if(y) {
return x + y
} else {
return x
}
}

3.3.2.默认参数

在 TypeScript 里,也可以为参数提供一个默认值,当用户没有传递这个参数或传递的值是 undefined 时。 它们叫做有默认初始化值的参数

1
2
3
function add(x: number, b: number = 6): number {
return x + y
}

3.3.3.剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,想同时操作多个参数,或者并不知道会有多少参数传递进来。 在 JavaScript 里,你可以使用 arguments 来访问所有传入的参数。

在 TypeScript 里,你可以把所有参数收集到一个变量里:
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。

1
2
3
4
function info(x: string, ...args: string[]) {
console.log(x, args)
}
info('abc', 'c', 'b', 'a')

3.4.函数重载

函数重载: 函数名相同, 而形参不同的多个函数
在JS中, 由于弱类型的特点和形参与实参可以不匹配, 是没有函数重载这一说的 但在TS中, 与其它面向对象的语言(如Java)就存在此语法

  • 在TypeScript中,可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用
  • 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 需求: 只能将两个数字/两个字符串进行相加
// 案例分析: any实现
function add(arg1, arg2) {
return arg1 + arg2
}

add(10, 20)
add("abc", "cba")
add({aaa: "aaa"}, 123)

// 1.实现两个函数
function add1(num1: number, num2: number) {
return num1 + num2
}

function add2(str1: string, str2: string) {
return str1 + str2
}

add1(10, 20)
add2("abc", "cba")


// 2.错误的做法: 联合类型是不可以
function add(arg1: number | string, arg2: number | string) {
return arg1 + arg2
}

// 3.TypeScript中函数的重载写法
// 3.1.先编写重载签名
function add(arg1: number, arg2: number): number
function add(arg1: string, arg2: string): string

// 3.2.编写通用的函数实现
function add(arg1: any, arg2: any): any {
return arg1 + arg2
}

add(10, 20)
add("aaa", "bbb")
// 通用函数不能被调用
// add({name: "why"}, "aaa")
// add("aaa", 111)

需求:定义一个函数,可以传入字符串或者数组,获取它们的长度

两种实现方案

  • 方案一:使用联合类型来实现
  • 方案二:实现函数重载来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1.普通的实现
function getLength(arg) {
return arg.length
}

// 2.函数的重载
function getLength(arg: string): number
function getLength(arg: any[]): number
function getLength(arg) {
return arg.length
}

// 3.联合类型实现(可以使用联合类型实现的情况, 尽量使用联合类型)
function getLength(arg: string | any[]) {
return arg.length
}

// 4.额外补充:对象类型实现
function getLength(arg: { length: number }) {
return arg.length
}

getLength("aaaaa")
getLength(["abc", "cba", "nba"])
getLength({ length: 100 })

3.5.调用签名

在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的

然而前面讲到的函数类型表达式并不能支持声明属性

如果想描述一个带有属性的函数,可以在一个对象类型中写一个调用签名(call signature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.函数类型表达式
type BarType = (num: number) => number

// 2.函数的调用签名(从对象的角度来看待这个函数, 也可以有其他属性)
interface IBar {
name: string
age: number
(num: number): number // 函数可以调用: 函数调用签名
}

const bar: IBar = (num: number): number => {
return num
}

bar.name = "aaa"
bar.age = 18
bar(123)

// 开发中如何选择:
// 1.如果只是描述函数类型本身(函数可以被调用), 使用函数类型表达式(Function Type Expressions)
// 2.如果在描述函数作为对象可以被调用, 同时也有其他属性时, 使用函数调用签名(Call Signatures)

3.6.构造签名

JavaScript 函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructor),因为 他们会产生一个新对象

  • 可以写一个构造签名( Construct Signatures ),方法是在调用签名前面加一个 new 关键词
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
constructor() { }
}

interface ICTORPerson {
new(): Person
}

function factory(fn: ICTORPerson) {
const f = new fn()
return f
}

factory(Person)

3.7.this相关

3.7.1.函数中this默认类型

tsconfig.json设置了noImplicitThistrue时, TypeScript会根据上下文推导this,但是在不能正确推导时,就会报错,需要明确的指定this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在没有对TS进行特殊配置的情况下, this是any类型

// 1.对象中的函数中的this
const obj = {
name: "why",
studying: function() {
// 默认情况下, this是any类型
console.log(this.name.length, "studying")
}
}

obj.studying()
// obj.studying.call({})

// 2.普通的函数
function foo() {
console.log(this)
}

3.7.2.函数中this明确类型

在开启noImplicitThis的情况下,必须指定this的类型

函数的第一个参数类型

  • 函数的第一个参数可以根据该函数之后被调用的情况,用于声明this的类型(名词必须叫this
  • 在后续调用函数传入参数时,从第二个参数开始传递的,this参数会在编译后被抹除
1
2
3
4
5
6
// 2.普通的函数
function foo(this: { name: string }, info: {name: string}) {
console.log(this, info)
}

foo.call({ name: "Tom" }, { name: "kobe" })

3.7.3.this相关内置工具

Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用

3.7.3.1.ThisParameterType

ThisParameterType

  • 用于提取一个函数类型Typethis (opens new window)参数类型
  • 如果这个函数类型没有this参数返回unknown
1
2
3
4
5
6
7
8
function foo(this: { name: string }, info: {name: string}) {
console.log(this, info)
}

type FooType = typeof foo

// ThisParameterType: 获取FooType类型中参数this的类型
type FooThisType = ThisParameterType<FooType> // type FooThisType = { name: string }

3.7.3.2.OmitThisParameter

OmitThisParameter

  • 用于移除一个函数类型Typethis参数类型, 并且返回当前的函数类型
1
2
3
4
5
6
7
8
function foo(this: { name: string }, info: {name: string}) {
console.log(this, info)
}

type FooType = typeof foo

// OmitOmitThisParameter: 删除this参数类型, 剩余的函数类型
type PureFooType = OmitThisParameter<FooType> // type PureFooType = (info: { name: string }) => void

3.7.3.3.ThisType

这个类型不返回一个转换过的类型,它被用作标记一个上下文的this类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ThisType: 用于绑定一个上下文的this
interface IState {
name: string
age: number
}

interface IStore {
state: IState
eating: () => void
running: () => void
}

const store: IStore & ThisType<IState> = {
state: {
name: "aaa",
age: 18
},
eating() {
console.log(this.name)
},
running() {
console.log(this.name)
}
}

store.eating.call(store.state) // aaa

4. 接口

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。我们使用接口(Interfaces)来定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)

4.1.接口初探

  • 在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型

  • 接口: 是对象的状态(属性)和行为(方法)的抽象(描述)

  • 接口类型的对象

    • 多了或者少了属性是不允许的

    • 可选属性: ?

    • 只读属性: readonly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 需求: 创建Person, 需要对人的属性进行一定的约束
* id是number类型, 必须有, 只读的
* name是string类型, 必须有
* age是number类型, 必须有
* sex是string类型, 可以没有
*/

// 定义人的接口
interface IPerson {
id: number
name: string
age: number
sex: string
}

const person1: IPerson = {
id: 1,
name: 'tom',
age: 20,
sex: '男'
}

类型检查器会查看对象内部的属性是否与IPerson接口描述一致, 如果不一致就会提示类型错误

4.2.可选属性

  • 接口里的属性不全都是必需的,有些是只在某些条件下存在,或者根本不存在。
  • 带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号
1
2
3
4
5
6
interface IPerson {
id: number
name: string
age: number
sex?: string
}

可选属性的优势

  • 可以对可能存在的属性进行预定义
  • 可以捕获引用了不存在的属性时的错误
1
2
3
4
5
6
const person2: IPerson = {
id: 1,
name: 'tom',
age: 20,
// gender: 'male' // 可以没有
}

4.3.只读属性

一些对象属性只能在对象刚刚创建的时候修改其值,可以在属性名前用 readonly 来指定只读属性:

1
2
3
4
5
6
interface IPerson {
readonly id: number
name: string
age: number
sex?: string
}

一旦赋值后再也不能被改变了

1
2
3
4
5
6
7
8
const person2: IPerson = {
id: 2,
name: 'tom',
age: 20,
// sex: '男' // 可以没有
// xxx: 12 // error 没有在接口中定义, 不能有
}
person2.id = 2 // error

readonly vs const

  • 最简单判断用 readonly 还是 const 的方法是,看要把它做为变量使用还是做为一个属性
  • 做为变量使用的话用 const,若做为属性则使用 readonly

4.4.函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

  • 为了使用接口表示函数类型,给接口定义一个调用签名
  • 它就像是一个只有参数列表和返回值类型的函数定义
  • 参数列表里的每个参数都需要名字和类型
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 
* 接口可以描述函数类型(参数的类型与返回的类型)
*/

interface SearchFunc {
(source: string, subString: string): boolean
}

const mySearch: SearchFunc = function (source: string, sub: string): boolean {
return source.search(sub) > -1
}

console.log(mySearch('abcd', 'bc'))

4.5.类实现接口

  • 接口定义后,也是可以被类实现的,使用implements
  • 如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
interface IKun {
name: string
age: number
slogan: string
playBasketball: () => void
}

interface IRun {
running: () => void
}

const ikun: IKun = {
name: "why",
age: 18,
slogan: "你干嘛!",
playBasketball: function () {
console.log("playing basketball")
}
}

// 作用: 接口被类实现
class Person implements IKun, IRun {
name: string
age: number
slogan: string

playBasketball() {
console.log("playing basketball")
}

running() {
console.log("running")
}
}

const ikun2 = new Person()
const ikun3 = new Person()
const ikun4 = new Person()
console.log(ikun2.name, ikun2.age, ikun2.slogan)
ikun2.playBasketball()
ikun2.running()

4.6.接口的继承

  • 接口和类一样是可以进行继承的,也是使用extends关键字
  • 接口是支持多继承的(类不支持多继承)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface IPerson {
name: string
age: number
}

// 可以从其他的接口中继承过来属性
// 1.减少了相同代码的重复编写
// 2.如果使用第三库, 给我们定义了一些属性
// > 自定义一个接口, 同时你希望自定义接口拥有第三方某一个类型中所有的属性
// > 可以使用继承来完成
interface IKun extends IPerson {
slogan: string
}

const ikun: IKun = {
name: "kun",
age: 18,
slogan: "你干嘛, 哎呦"
}

5. 类

对于传统的 JavaScript 程序我们会使用函数基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员使用这些语法就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从 ECMAScript 2015,也就是 ES6 开始, JavaScript 程序员将能够使用基于类的面向对象的方式。 使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本。

5.1.定义

类的声明:使用class关键字

声明类的属性:在类的内部声明类的属性以及对应的类型

  • 如果类型没有声明,那么它们默认是any的

  • 也可以给属性设置初始化值

  • 在默认的strictPropertyInitialization模式下面我们的属性是必须 初始化的,如果没有初始化,那么编译时就会报错

    • 如果在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用 name!: string语法
  • 类可以有自己的构造函数constructor,通过new关键字创建 一个实例时,构造函数会被调用

  • 构造函数不需要返回任何值,默认返回当前创建出来的实例

  • 类中可以有自己的函数,定义的函数称之为方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
name!: string
age: number

constructor(name: string, age: number) {
// this.name = name
this.age = age
}

running() {
console.log(this.name + " running!")
}

eating() {
console.log(this.name + " eating!")
}
}

5.2.继承

  • 面向对象的其中一大特性就是继承,继承不仅仅可以减少代码量,也是多态的使用前提

  • 使用extends关键字来实现继承,子类中使用super来访问父类

  • 举例一下Student类继承自Person

    • Student类可以有自己的属性和方法,并且会继承Person的属性和方法

    • 在构造函数中,可以通过super来调用父类的构造方法,对父类中的属性进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person {
name!: string
age: number

constructor(name: string, age: number) {
this.name = name
this.age = age
}

running() {
console.log(this.name + " running!")
}

eating() {
console.log(this.name + " eating!")
}
}

class Student extends Person {
sno: number

constructor(name: string, age: number, sno: numebr) {
super(name, age)
this.sno = sno
}

studying() {
console.log(this.name + " studying!")
}

running() {
super.running()
console.log("Student running!")
}

eating() {
console.log("Student eating!")
}
}

5.3.多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Animal {
name: string

constructor (name: string) {
this.name = name
}

run(distance: number = 0) {
console.log(`${this.name} run ${distance}m`)
}
}

class Snake extends Animal {
constructor (name: string) {
// 调用父类型构造方法
super(name)
}

// 重写父类型的方法
run(distance: number = 5) {
console.log('sliding...')
super.run(distance)
}
}

class Horse extends Animal {
constructor(name: string) {
// 调用父类型构造方法
super(name)
}

// 重写父类型的方法
run(distance: number = 50) {
console.log('dashing...')
// 调用父类型的一般方法
super.run(distance)
}

xxx() {
console.log('xxx()')
}
}

const snake = new Snake('sn')
snake.run()

const horse = new Horse('ho')
horse.run()

/**
* 多态
* 父类型的引用指向了子类型的对象,不同类型的对象针对相同的方法,产生了不同的行为
**/

// 1.父类型引用指向子类型的实例 => 多态
const tom: Animal = new Horse('ho22')
tom.run()

/* 2.如果子类型没有扩展的方法, 可以让子类型引用指向父类型的实例 */
const tom2: Snake = new Animal('tom2')
tom2.run()
/* 3.如果子类型有扩展的方法, 不能让子类型引用指向父类型的实例 */
const tom3: Horse = new Animal('tom3')
tom3.run()

5.4.修饰符

在TypeScript中,类的属性和方法支持三种修饰符: publicprivateprotected

  • public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public,可以直接访问
  • private 修饰的是仅在同一类中可见、私有的属性或方法
  • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** 
* 访问修饰符: 用来描述类内部的属性/方法的可访问性
* public: 默认值, 公开的外部也可以访问
* private: 只能类内部可以访问
* protected: 类内部和子类可以访问
**/

class Person {
protected name: string
private age: number
public gerder: string

constructor(name: string, age: number, gender: string) {
this.name = name
this.age = age
this.gender = gender
}

// 方法变成私有方法: 只有在类内部才能访问
private eating() {
console.log("吃东西", this.age, this.name)
}
}

const p = new Person("aaa", 18)
// console.log(p.name, p.age)
// p.name = "kobe"
// p.eating()

class Student extends Person {
constructor(name: string, age: number) {
super(name, age)
}

studying() {
console.log("在学习", this.name)
}
}

const stu = new Student("bbb", 18)

5.5.readonly

你可以使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化

1
2
3
4
5
6
7
8
9
class Person {
readonly name: string = 'abc'
constructor(name: string) {
this.name = name
}
}

const person = new Person('cba')
// person.name = 'peter' // error

5.6.参数属性

TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性

  • 这些被称为参数属性(parameter properties)
  • 可以通过在构造函数参数前添加一个可见性修饰符 publicprivateprotected或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
// 语法糖
constructor(public name: string, private _age: number, readonly height: number, protected gender: string) {
}

running() {
console.log(this._age, "eating")
}
}

const p = new Person("aaa", 18, 1.88, "male")
console.log(p.name, p.height)

// p.height = 1.98

5.7.存取器

在前面一些私有属性是不能直接访问的,或者某些属性想要监听它的获取(getter)和设置(setter)的过程,这个时候可以使用存取器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Person {
// 私有属性: 属性前面会使用_
private _name: string
private _age: number

constructor(name: string, age: number) {
this._name = name
this._age = age
}

running() {
console.log("running:", this._name)
}

// setter/getter: 对属性的访问进行拦截操作
set name(newValue: string) {
this._name = newValue
}

get name() {
return this._name
}


set age(newValue: number) {
if (newValue >= 0 && newValue < 200) {
this._age = newValue
}
}

get age() {
return this._age
}
}

const p = new Person("aaa", 100)
p.name = "kobe"
console.log(p.name) // "koba"

p.age = -10
console.log(p.age) // 100

5.8.静态属性

到目前为止,只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上,使用static关键字

使用 类名.静态属性名 来访问静态属性

1
2
3
4
5
6
7
8
9
10
11
12
/** 
* 静态属性, 是类对象的属性
* 非静态属性, 是类的实例对象的属性
*/

class Person {
name1: string = 'A'
static name2: string = 'B'
}

console.log(Person.name2)
console.log(new Person().name1)

5.9.抽象类

继承是多态使用的前提

  • 所以在定义很多通用的调用接口时, 通常会让调用者传入父类,通过多态来实现更加灵活的调用方式
  • 但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,可以定义为抽象方法

抽象方法:在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法

  • 抽象方法,必须存在于抽象类中
  • 抽象类是使用abstract声明的类

抽象类有如下的特点

  • 抽象类做为其它派生类的基类使用

  • 抽象类可以包含成员的实现细节

  • 抽象类是不能被实例的话(也就是不能通过new创建)

  • 抽象方法必须被子类实现,则该类必须是一个抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
abstract class Shape {
// getArea方法只有声明没有实现体
// 实现让子类自己实现
// 可以将getArea方法定义为抽象方法: 在方法的前面加abstract
// 抽象方法必须出现在抽象类中, 类前面也需要加abstract
abstract getArea(): number
}

class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super()
}

getArea() {
return this.width * this.height
}
}

class Circle extends Shape {
constructor(public radius: number) {
super()
}

getArea() {
return this.radius ** 2 * Math.PI
}
}

class Triangle extends Shape {
constructor(public a: number, public b: number, public c: number) {
super()
}

getArea() {
let p = 0
let s = 0
if (this.a + this.b < this.c || this.a + this.c < this.b || this.b + this.c < this.a) {
throw new Error("不能构成三角形")
} else {
p = (this.a + this.b + this.c) / 2;
s = Math.sqrt(p * (p - this.a) * (p - this.b) * (p - this.c));
return s
}
}
}


// 通用的函数
function calcArea(shape: Shape) {
return shape.getArea()
}

calcArea(new Rectangle(10, 20))
calcArea(new Circle(5))
calcArea(new Triangle(3, 4, 5))

// 在Java中会报错: 不允许
calcArea({ getArea(): number { return 1 } })

// 抽象类不能被实例化
// calcArea(new Shape())
// calcArea(100)
// calcArea("abc"

5.10.类的类型

类本身也是可以作为一种数据类型的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
name: string
constructor(name: string) {
this.name = name
}

running() {
console.log(this.name + " running!")
}
}

const p1 = new Person("aaa")
const p2: Person = {
name: "bbb",
running() {
console.log(this.name + " running!")
}
}

5.11.TS类型检测-鸭子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// TypeScript对于类型检测的时候使用的鸭子类型
// 鸭子类型: 如果一只鸟, 走起来像鸭子, 游起来像鸭子, 看起来像鸭子, 那么你可以认为它就是一只鸭子
// 鸭子类型, 只关心属性和行为, 不关心你具体是不是对应的类型

class Person {
constructor(public name: string, public age: number) { }

running() { }
}

class Dog {
constructor(public name: string, public age: number) { }
running() { }
}

function printPerson(p: Person) {
console.log(p.name, p.age)
}

printPerson(new Person("why", 18))
// printPerson("abc")
printPerson({ name: "kobe", age: 30, running: function () { } })
printPerson(new Dog("旺财", 3))

const person: Person = new Dog("果汁", 5)

5.12.对象类型属性修饰符

对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息

  • 可选属性(Optional Properties)
    • 在属性名后面加一个 ? 标记表示这个属性是可选的
  • 只读属性(Readonly Properties)
    • 在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为
    • 但在类型检查的时候,一个标记为 readonly的属性是不能被写入的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义对象类型
type IPerson = {
// 属性?: 可选的属性
name?: string
// readonly: 只读的属性
readonly age: number
}

interface IKun {
name?: string
readonly slogan: string
}

const p: IPerson = {
name: "why",
age: 18
}

// p.age = 30

5.13.索引签名

索引签名的含义

  • 有的时候,不能提前知道一个类型里的所有属性的名字,但是知道这些值的特征
  • 这种情况,就可以用一个索引签名 (index signature) 来描述可能的值的类型

索引签名的用法

  • 一个索引签名的属性类型必须是 string 或者是 number
  • 虽然 TypeScript 可以同时支持 stringnumber 类型,但数字索引的返回类型一定要是字符索引返回类型的子类型

5.13.1.基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1.索引签名的理解
interface InfoType {
// 索引签名: 可以通过字符串索引, 去获取到一个值, 也是字符串
[key: string]: string
}

function getInfo(): InfoType {
const abc: any = "haha"
return abc
}

const info = getInfo()
console.log(info.name, info.age, info.address)

// 2.索引签名的案例
interface ICollection {
[index: number]: string
length: number
}

function printCollection(collection: ICollection) {
for (let i = 0; i < collection.length; i++) {
const item = collection[i]
console.log(item.length)
}
}

const array = ["abc", "cba", "nba"]
const tuple: [string, string] = ["aaa", "北京"]
printCollection(array)
printCollection(tuple)

5.13.2.类型问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface IIndexType {
// 返回值类型的目的是告知通过索引去获取到的值是什么类型
// [index: number]: string
// [index: string]: any
// [index: string]: string
}

// 索引签名: [index: number]: string 没有报错
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: any 没有报错
// 索引要求必须是字符串类型 names[0] => names["0"]
// const names: IIndexType = ["abc", "cba", "nba"]

// 索引签名: [index: string]: string 会报错
// 严格字面量赋值检测: ["abc", "cba", "nba"] => Array实例 => names[0], names.forEach
// const names: IIndexType = ["abc", "cba", "nba"]
// names["forEach"] => function
// names["map/filter"] => function

5.13.3.两个签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface IIndexType {
// 两个索引类型的写法
[index: number]: string
[key: string]: any

// 要求一: 数字类型索引的类型, 必须是字符串类型索引的类型的 子类型
// 结论: 数字类型必须是比字符串类型更加确定的类型(需要是字符串类型的子类型)
// 原因: 所有的数字类型都是会转成字符串类型去对象中获取内容

// [index: number]: string
// [key: string]: string | number

// 要求二: 如果索引签名中有定义其他属性, 其他属性返回的类型, 必须符合string类型返回的属性
// [index: number]: string
// [key: string]: string | number

// aaa: string 符合要求
// bbb: boolean 错误的类型
}

const names: IIndexType = ["abc", "cba", "nba"]
const item1 = names[0]
const forEachFn = names["forEach"]

names["aaa"]

5.14.严格的字面量赋值检测

This PR implements stricter obiect literal assianment checks for the purpose of catching excess or misspelled properties.

The PR implements the suggestions in #3755. Specifically:

  • Every object literal is initially considered “fresh”
  • When a fresh object literal is assigned to a variable or passed for a parameter of a non-empty target type, it is an error for the object literal to specify properties that don’t exist in the target type.
  • Freshness disappears in a type assertion or when the type of an object literal is widened.

简单对上面的英文进行翻译解释

  • 每个对象字面量最初都被认为是“新鲜的(fresh)”
  • 当一个新的对象字面量分配给一个变量或传递给一个非空目标类型的参数时,对象字面量指定目标类型中不存在的属性是错误的
  • 当类型断言或对象字面量的类型扩大时,新鲜度会消失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface IPerson {
name: string
age: number
}

// 1.奇怪的现象一:
// 定义info, 类型是IPerson类型
const obj = {
name: "aaa",
age: 18,

// 多了一个height属性
height: 1.88
}
const info: IPerson = obj // 没有报错

// 2.奇怪的现象二:
function printPerson(person: IPerson) { }
const kobe = { name: "kobe", age: 30, height: 1.98 }
printPerson(kobe) // 没有报错

// 解释现象
// 第一次创建的对象字面量, 称之为fresh(新鲜的)
// 对于新鲜的字面量, 会进行严格的类型检测. 必须完全满足类型的要求(不能有多余的属性)
const obj2 = {
name: "why",
age: 18,

height: 1.88
}

const p: IPerson = obj2

6.泛型

在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定具体类型的一种特性

虽然any是可以的,但是定义为any的时候,其实已经丢失了类型信息

  • 比如传入的是一个number,那么希望返回的可不是any类型,而是number类型
  • 所以,需要在函数中可以捕获到参数的类型是number,并且同时使用它来作为返回值的类型

6.1.类型的参数化

  • 通过 <类型> 的方式将类型传递给函数
  • 通过类型推导(type argument inference),自动推到出传入变量的类型
    • 在这里会推导出它们是字面量类型的,因为字面量类型对于函数也是适用的
1
2
3
4
5
6
7
8
9
10
11
12
function bar<Type>(arg: Type): Type {
return arg
}

// 1.完整的写法
const res1 = bar<number>(123)
const res2 = bar<string>("abc")
const res3 = bar<{name: string}>({ name: "aaa" })

// 2.省略的写法
const res4 = bar("aaa")
const res5 = bar(111)

react中useState的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 元组: useState函数
function useState<Type>(initialState: Type): [Type, (newState: Type) => void] {
let state = initialState
function setState(newState: Type) {
state = newState
}

return [state, setState]
}

// 初始化count
const [count, setCount] = useState(100)
const [message, setMessage] = useState("Hello World")
const [banners, setBanners] = useState<any[]>([])

6.2.多个泛型参数

一个函数可以定义多个泛型参数

平时在开发中可能会看到一些常用的名称

  • T:Type的缩写,类型
  • KVkeyvalue的缩写,键值对
  • EElement的缩写,元素
  • OObject的缩写,对象
1
2
3
4
function foo <K, V> (key: K, value: V): [K, V] {
return [key, value]
}
const result = foo<string, number>('age', 18)

6.3.泛型接口

在定义接口时, 为接口中的属性或方法定义泛型类型
在使用接口时, 再指定具体的泛型类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface IKun<Type = string> {
name: Type
age: number
slogan: Type
}

const ikun1: IKun<string> = {
name: "kun",
age: 18,
slogan: "哎呦,你干嘛!"
}

const ikun2: IKun<number> = {
name: 123,
age: 20,
slogan: 666
}

// 没有赋值具体的类型,使用Type的默认值(<Type = string>)
// Type没有默认值时,会报错,不会自动推断出类型
const ikun3: IKun = {
name: "ikun",
age: 30,
slogan: "坤坤加油!"
}

6.4.泛型类

在定义类时, 为类中的属性或方法定义泛型类型 在创建类的实例时, 再指定特定的泛型类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point<Type = number> {
x: Type
y: Type
constructor(x: Type, y: Type) {
this.x = x
this.y = y
}
}

// 没有赋值具体的类型,使用Type的默认值(<Type = number>)
// Type没有默认值时,会自动推断出类型,不会报错
const p1 = new Point(10, 20)
console.log(p1.x)
const p2 = new Point("123", "321")
console.log(p2.x)

6.5.泛型约束

希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中

  • 比如stringarray都是有length的,或者某些对象也是会有length属性的
  • 那么只要是拥有length的属性都可以作为的参数类型
  • 这里表示是传入的类型必须有这个属性,也可以有其他属性,但是必须至少有这个成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface ILength {
length: number
}

// 1.getLength没有必要用泛型
function getLength(arg: ILength) {
return arg.length
}

const length1 = getLength("aaaa")
const length2 = getLength(["aaa", "bbb", "ccc"])
const length3 = getLength({ length: 100 })


// 2.获取传入的内容, 这个内容必须有length属性
// Type相当于是一个变量, 用于记录本次调用的类型, 所以在整个函数的执行周期中, 一直保留着参数的类型
function getInfo<Type extends ILength>(args: Type): Type {
return args
}

const info1 = getInfo("aaaa")
const info2 = getInfo(["aaa", "bbb", "ccc"])
const info3 = getInfo({ length: 100 })

在泛型约束中使用类型参数(Using Type Parameters in Generic Constraints)

  • 可以声明一个类型参数,这个类型参数被其他类型参数约束
  • 举个栗子:希望获取一个对象给定属性名的值
    • 需要确保不会获取 obj 上不存在的属性
    • 所以在两个类型之间建立一个约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 传入的key类型, obj当中key的其中之一
interface IKun {
name: string
age: number
}

type IKunKeys = keyof IKun // "name"|"age" => 类似于 K = "name" | "age"

function getObjectProperty<O, K extends keyof O>(obj: O, key: K) {
return obj[key]
}

const info = {
name: "aaa",
age: 18,
height: 1.88
}

const name = getObjectProperty(info, "name")

6.6.映射类型

有的时候,一个类型需要基于另外一个类型,但是又不想拷贝一份,这个时候可以考虑使用映射类型

  • 大部分内置的工具都是通过映射类型来实现的
  • 大多数类型体操的题目也是通过映射类型完成的

映射类型建立在索引签名语法基础之上

  • 映射类型,就是使用了 PropertyKeys 联合类型的泛型
  • 其中 PropertyKeys 多是通过 keyof 创建,然后循环遍历键名创建一个类型

6.6.1.基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TypeScript提供了映射类型: 函数
// 映射类型不能使用interface定义
// Type = IPerson
// keyof = "name" | "age"
type MapPerson<Type> = {
// 索引类型以此进行使用
[Property in keyof Type]: Type[Property]
}

interface IPerson {
name: string
age: number
}

type NewPerson = MapPerson<IPerson>

6.6.2.修饰符使用

在使用映射类型时,有两个额外的修饰符可能会用到

  • 一个是 readonly,用于设置属性只读
  • 一个是 ? ,用于设置属性可选
1
2
3
4
5
6
7
8
9
10
11
12
type MapPerson<Type> = {
readonly [Property in keyof Type]?: Type[Property]
}

interface IPerson {
name: string
age: number
height: number
address: string
}

type IPersonOptional = MapPerson<IPerson>

6.6.3.修饰符符号

可以通过前缀 - 或者 + 删除或者添加这些修饰符

如果没有写前缀,相当于使用了 + 前缀(默认)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type MapPerson<Type> = {
-readonly [Property in keyof Type]-?: Type[Property]
}

interface IPerson {
name: string
age?: number
readonly height: number
address?: string
}

type IPersonRequired = MapPerson<IPerson>

const p: IPersonRequired = {
name: "aaa",
age: 18,
height: 1.88,
address: "北京市"
}

7.内置工具与类型体操

  • 类型系统其实在很多语言里面都是有的,比如Java、Swift、C++等等,但是相对来说TypeScript的类型非常灵活

    • 这是因为TypeScript的目的是为JavaScript添加一套类型校验系统,因为JavaScript本身的灵活性,也让TypeScript类型系统 不得不增加更附加的功能以适配JavaScript的灵活性
    • 所以TypeScript是一种可以支持类型编程的类型系统
  • 这种类型编程系统为TypeScript增加了很大的灵活度,同时也增加了它的难度

    • 如果你不仅仅在开发业务的时候为自己的JavaScript代码增加上类型约束,那么基本不需要太多的类型编程能力
    • 但是如果你在开发一些框架、库,或者通用性的工具,为了考虑各种适配的情况,就需要使用类型编程
  • TypeScript本身为我们提供了类型工具,帮助我们辅助进行类型转换(前面有用过关于this的类型工具)

  • 很多开发者为了进一步增强自己的TypeScript编程能力,还会专门去做一些类型体操的题目

7.1.条件类型

很多时候,日常开发中需要基于输入的值来决定输出的值,同样也需要基于输入的值的类型来决定输出的值的类型

条件类型(Conditional types)就是用来描述输入类型和输出类型之间的关系

  • 条件类型的写法有点类似于 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression ): 条件类型(Conditional Types)
  • SomeType extends OtherType ? TrueType : FalseType;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type IDType = number | string

// 判断number是否是extendsIDType
// const res = 2 > 3? true: false
type ResType = boolean extends IDType ? true : false // type ResType = false

// 举个栗子: 函数的重载
// function sum(num1: number, num2: number): number
// function sum(num1: string, num2: string): string

// 错误的做法: 类型扩大化
// function sum(num1: string|number, num2: string|number): string

function sum<T extends number | string>(num1: T, num2: T): T extends number ? number : string
function sum(num1: any, num2: any) {
return num1 + num2
}

const res = sum(20, 30)
const res2 = sum("abc", "cba")
// const res3 = sum(123, "cba") //error

7.2.条件类型中的推断

在条件类型中推断(Inferring Within Conditional Types)

  • 条件类型提供了 infer 关键词,可以从正在比较的类型中推断类型,然后在 true 分支里引用该推断结果

比如现在有一个函数类型,想要获取到一个函数的参数类型和返回值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type CalcFnType = (num1: numebr, num2: number) => number

function foo() {
return "abc"
}

// 返回函数的返回值类型 内置工具为 ReturnType
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never
// 返回函数的参数类型
type MyParameterType<T extends (...args; any[]) => any> = T extends (...args: infer P) => any ? P : never

// 获取一个函数的返回值类型: 内置工具
type CalcReturnType = MyReturnType<CalcFnType>
type FooReturnType = MyReturnType<typeof foo>
// type FooReturnType2 = MyReturnType<boolean>

type CalcParameterType = MyParameterType<CalcFnType>

7.3.分发条件类型

当在泛型中使用条件类型的时候,如果传入一个联合类型,就会变成 分发的(distributive)

如果在 ToArray 传入一个联合类型,这个条件类型会被应用到联合类型的每个成员

  • 当传入string | number时,会遍历联合类型中的每一个成员
  • 相当于ToArray | ToArray
  • 所以最后的结果是:string[] | number[]
1
2
3
4
5
6
7
type toArray<T> = T extends any ? T[] : never
// number[] | string[]

type NumArray = toArray<number> // type NumArray = number[]

// number[] | string[] 而不是 (number | string)[]
type NumAndStrArray = toArray<number | string> // type NumAndStrArray = number[] | string[]

7.4.内置工具

7.4.1.Partial

用于构造一个Type下面的所有属性都设置为可选的类型

格式:Partial<Type>

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IKun {
name: string
age: number
slogan?: string
}

type MyPartial<T> = {
[P in keyof T]?: T[P]
}

// IKun都变成可选的
// type IKunOptional = Partial<IKun>
type IKunOptional = MyPartial<IKun>

7.4.2.Required

用于构造一个Type下面的所有属性全都设置为必填的类型,这个工具类型跟 Partial 相反

格式:Required<Type>

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IKun {
name: string
age: number
slogan?: string
}

type MyRequired<T> = {
[P in keyof T]-?: T[P]
}

// IKun都变成可选的
// type IKunRequire = Required<IKun>
type IKunRequire = MyRequired<IKun>

7.4.3.Readonly

用于构造一个Type下面的所有属性全都设置为只读的类型,意味着这个类型的所有的属性全都不可以重新赋值

格式:Readonly<Type>

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IKun {
name: string
age: number
slogan?: string
}

type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}

// IKun都变成只读的
// type IKunReadonly = Readonly<IKun>
type IKunReadonly = MyReadonly<IKun>

7.4.4.Record

用于构造一个对象类型,它所有的key(键)都是Keys类型,它所有的value(值)都是Type类型。

格式:Record<Keys, Type>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface IKun {
name: string
age: number
slogan?: string
}

type Keys = keyof IKun // type Keys = "name" | "age" | "slogan"
type Res = keyof any // type Res = number|string|symbol

// 确实keys一定是可以作为key的联合类型
type MyRecord<Keys extends keyof any, T> = {
[P in Keys]: T
}

// IKun变成遍布各地的
type Citys = "上海" | "北京" | "洛杉矶"
// type IKuns = Record<Citys, IKun>
type IKuns = MyRecord<Citys, IKun>

const ikuns: IKuns = {
"上海": {
name: "xxx",
age: 10
},
"北京": {
name: "yyy",
age: 5
},
"洛杉矶": {
name: "zzz",
age: 3
}
}

7.4.5.Pick

用于构造一个类型,它是从Type类型里面挑了一些属性Keys

格式:Pick<Type, Keys>

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IKun {
name: string
age: number
slogan?: string
}

type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}

// 挑选IKun的一些属性
// type IKuns = Pick<IKun, "slogan" | "name">
type IKuns = MyPick<IKun, "slogan" | "name">

7.4.6.Omit

用于构造一个类型,它是从Type类型里面过滤(排除)一些属性Keys

用法:Omit<Type, Keys>

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IKun {
name: string
age: number
slogan?: string
}

type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}

// 排除IKun的一些属性
// type IKuns = Omit<IKun, "slogan" | "name">
type IKuns = MyOmit<IKun, "slogan" | "name">

7.4.7.Exclude

用于构造一个类型,它是从UnionType联合类型里面排除了所有可以赋给ExcludedMembers的类型

用法:Exclude<UnionType, ExcludedMembers>

1
2
3
4
5
6
7
type IKun = "sing" | "dance" | "rap"

type MyExclude<T, E> = T extends E ? never : T

// 排除IKun的一些字面量类型
// type IKuns = Exclude<IKun, "rap">
type IKuns = MyExclude<IKun, "rap"> // type IKuns = "sing" | "dance"

7.4.8.Extract

用于构造一个类型,它是从Type类型里面提取了所有可以赋给Union的类型

用法:Extract<Type, Union>

1
2
3
4
5
6
7
type IKun = "sing" | "dance" | "rap"

type MyExtract<T, E> = T extends E ? T : never

// 提取包含IKun的一些字面量类型
// type IKuns = Extract<IKun, "sing" | "dance">
type IKuns = MyExtract<IKun, "sing" | "dance"> // type IKuns = "sing" | "dance"

7.4.9.NonNullable

用于构造一个类型,这个类型从Type中排除了所有的nullundefined的类型

用法:NonNullable<T>

1
2
3
4
5
6
7
type IKun = "sing" | "dance" | "rap" | null | undefined

type MyNonNullable<T> = T extends null | undefined ? never : T

// 排除IKun字面量属性中的null和undefined
// type IKuns = NonNullable<IKun>
type IKuns = MyNonNullable<IKun> // type IKuns = "sing" | "dance" | "rap"

7.4.10.ReturnType

用于构造一个含有Type函数的返回值的类型

用法:ReturnType<T>

1
2
3
4
5
6
7
8
9
function iKun() {
return "哎呦,你干嘛!"
}

type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never

// 获取ikun函数的返回值
// type iKunReturnType = ReturnType<typeof iKun>
type iKunReturnType = MyReturnType<typeof iKun> // type iKunReturnType = string

7.4.11.InstanceType

用于构造一个由所有Type的构造函数的实例类型组成的类型

用法:InstanceType<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person { }
class Dog { }

type MyInstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : never

const p1: Person = new Person()

// typeof Person: 构造函数具体的类型
// InstanceType构造函数创建出来的实例对象的类型
type MyPerson = MyInstanceType<typeof Person> // type MyPerson = Person
const p2: MyPerson = new Person()

// 构造函数的例子
// 通过创建实例的工具函数时会用到这个InstanceType
function factory<T extends new (...args: any[]) => any>(ctor: T): HYInstanceType<T> {
return new ctor()
}

const p3 = factory(Person) // typeof p3 = Person
const d = factory(Dog) // typeof d = Dog

7.4.12.Parameters

用于获取函数的所有参数

用法:Parameters<T>

1
2
3
4
5
6
7
8
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

function test(a: string, b: number) {
return a + b
}

type ParamsType = Parameters<typeof test>
// type ParamsType = [a: string, b: number]

7.5.自定义工具

7.5.1.ChangeFunctionReturn

用于改变一个函数的返回值类型

用法:ChangeFunctionReturn<F, R>

1
2
3
4
5
6
7
8
9
10
type ChangeFunctionReturn<F extends (...args: any[]) => any, R = any> = (
...args: Parameters<F>
) => R

function test(a: string, b: number) {
return a + b
}

type TestFnType = ChangeFunctionReturn<typeof test, Array<string>>
// type TesFnType = (a: string, b: number) => string[]

7.5.2.ChangeFunctionParameters

用于改变一个函数的参数类型

用法:ChangeFunctionParameters<F, P>

1
2
3
4
5
6
7
8
9
10
11
12
13
type ChangeFunctionParameters<
F extends (...args: any[]) => any,
P extends Array<any>
> = (...args: P) => ReturnType<F>

function test(a: string, b: number) {
return a + b
}

type ParametersType = [x: string, y: number, z: number]

type TestFnType = ChangeFunctionParameters<typeof test, ParametersType>
// type TestType = (x: string, y: number, z: number) => string

7.5.3.DeepReadonly

用于构造一个Type下面的所有属性(包括深层Type)全都设置为只读的类型

用法:DeepReadonly<F>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type DeepReadonly<T extends Record<string | symbol, any>> = {
readonly [K in keyof T]: DeepReadonly<T[K]>;
}

interface IKun {
name: string
age: number
hobbies: {
sing: string
dance: string
rap: string
}
}

// IKun都变成只读的, 包括 sing, dance, rap
type ReadonlyIKun = DeepReadonly<IKun>

8. 知识拓展

6.1.声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能

什么是声明语句

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $jQuery 了。

但是在 ts 中,编译器并不知道 $ 或 jQuery 是什么东西

1
2
3
4
5
6
7
8
9
10
/* 
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明语句: 如果需要ts对新的语法进行检查, 需要要加载了对应的类型说明代码
declare var jQuery: (selector: string) => any;
声明文件: 把声明语句放到一个单独的文件(jQuery.d.ts)中, ts会自动解析到项目中所有声明文件
下载声明文件: npm install @types/jquery --save-dev
*/

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

这时,我们需要使用 declare var 来定义它的类型

1
2
3
declare var jQuery: (selector: string) => any;

jQuery('#foo');

declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

1
jQuery('#foo');

一般声明文件都会单独写成一个 xxx.d.ts 文件

创建 01_jQuery.d.ts, 将声明语句定义其中, TS编译器会扫描并加载项目中所有的TS声明文件

1
declare var jQuery: (selector: string) => any;

很多的第三方库都定义了对应的声明文件库, 库文件名一般为 @types/xxx, 可以在 https://www.npmjs.com/package/package 进行搜索

有的第三库在下载时就会自动下载对应的声明文件库(比如: webpack),有的可能需要单独下载(比如jQuery/react)

6.2.内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。

内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。

  1. ECMAScript 的内置对象

Boolean
Number
String
Date
RegExp
Error

1
2
3
4
5
6
7
8
9
/* 1. ECMAScript 的内置对象 */
let b: Boolean = new Boolean(1)
let n: Number = new Number(true)
let s: String = new String('abc')
let d: Date = new Date()
let r: RegExp = /^1/
let e: Error = new Error('error message')
b = true
// let bb: boolean = new Boolean(2) // error
  1. BOM 和 DOM 的内置对象

Window
Document
HTMLElement
DocumentFragment
Event
NodeList

1
2
3
4
5
6
const div: HTMLElement = document.getElementById('test')
const divs: NodeList = document.querySelectorAll('div')
document.addEventListener('click', (event: MouseEvent) => {
console.dir(event.target)
})
const fragment: DocumentFragment = document.createDocumentFragment()