class 类

TypeScript 有 class 关键字,并为其添加了类型注解和其他语法

  1. TS 中的类型推论可以得到 Human 类的实例对象 p 的类型是 Human
  2. TS 中的 class,不仅提供了 class 的语法功能,也作为一种类型存在

image.png

  1. 声明成员 age,类型为 number(没有初始值)
  2. 声明成员 gender,没有类型注解,但是设置了初始值,TS 推论为 string 类型
1
2
3
4
5
class Human {
age: number
gender = 'man'
// gender: string = 'man'
}

构造函数

  1. 成员初始化(例如 age: number)后,才可以通过this来访问实例对象
  2. 需要为构造函数指定类型注解,否则会被定义为 any 类型;构造函数不需要返回值类型
1
2
3
4
5
6
7
8
9
class Person {
age: number
gender: string

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

实例方法:方法的类型注释与函数用法一致

1
2
3
4
5
6
7
8
9
class Point {
x = 10
y = 10

scale(n: number): void {
this.x *= n
this.y *= n
}
}

只读修饰符 readonly:表示只读,用来防止在构造函数之外属性进行赋值

  1. 使用 readonly 关键字修饰该属性是只读的,不能修饰方法。
  2. 属性后面的类型注解如果不加,则类型为字面量类型。 例如下面的 age 不加类型注解,age 的类型为 18
  3. 接口或者 {} 表示的对象类型,也可以使用 readonly。
1
2
3
4
5
6
class Person {
readonly age: number = 18
constructor(age: number) {
this.age = age
}
}

类继承

extends 继承父类

  1. 通过 extends 关键字实现继承
  2. 子类继承父类,则之类的实例对象就同时拥有父类和子类的所以属性和方法
1
2
3
4
5
6
7
class Animal {
move() { console.log('Moving along!')}
}
class Dog extends Animal {
bark() { console.log('汪!')}
}
const dog = new Dog()

implements 实现接口

  1. 通过 implements 关键字让 class 实现接口
  2. 类实现接口,要求类中必须提供接口中指定的所以方法和属性
1
2
3
4
5
6
7
8
interface Singable {
sing(): void
}
class Person implements Singable {
sing() {
console.log('就这样被你征服')
}
}

类成员可见性

可以使用 TS 来控制 class 的方法和属性对于 class 外的代码是否可见

可见性修饰符包括:public(公有)、protected(受保护的)、private(私有的)

public:公有成员可以被任何地方访问,默认的可见性

  1. 在类属性或方法前面添加 public 关键字,来修饰属性或方法为公有的
  2. 因为 public 是默认可见性,所以通常省略不写

protected:受保护的,仅对其声明所在类和子类(非实例对象)当中可见

  1. protected 修饰,表示该属性或方法是受保护的
  2. 在子类的方法中可以通过 this 来访问父类中受保护的成员,但是实例不可见
1
2
3
4
5
6
7
8
9
class Animal {
protected move() { console.log('爬')}
}
class Dog extends Animal {
bark() {
console.log('汪!')
this.move() // 可以通过this访问父类中受保护的成员
}
}

private:私有的,只在当前类可见

  1. 添加 private 关键字,表示该属性或方法是私有的
  2. 私有的属性或方法只能在当前类中可见,对子类或者实例都不可见
1
2
3
4
5
6
class Animal {
private move() { console.log('爬')}
walk() {
this.move() // 可以在当前类通过this访问私有成员
}
}

类型兼容性

两种类型系统:

  1. Structural Type System(结构化类型系统)
  2. Nominal Type System(标明类型系统)

TS 采用的是结构化类型系统,类型检查关注的是值所具有的形状

1
2
3
class Point { x: number; y: number }
class Point2D { x: number; y: number }
const p: Point = new Point2D()
  1. Point 和 Point2D 是两个名称不同的类
  2. 变量 P 的类型被标记为 Point 类型,其值与 Point2D 一致,所以不会报错,因为它们的结构相同
  3. 如果在标明类型系统(如 C#、Java等),它们就是不同的类,类型无法兼容

对于对象类型来说,成员多的可以赋值给少的

1
2
3
class Point { x: number; y: number }
class Point3D { x: number; y: number; z: number }
const p: Point = new Point3D()

接口兼容性

接口之间的兼容性类似于 class,并且 class 和 interface 之间也可以兼容,属性多的可以赋值给少的

1
2
3
4
5
6
7
8
9
interface Point { x: number; y: number }
interface Point2D { x: number; y: number }
let p1: Point
let p2: Point2D = p1

interface Point3D { x: number; y: number; z: number }
let p3: Point3D
p2 = p3
let p4: Point2D = new Point3D()

函数兼容性

函数之间兼容性比较复杂,需考虑:

  1. 参数个数,参数多的兼容参数少的(参数少的可以赋值给多的),与接口不同
1
2
3
4
type F1 = (a: number) => void
type F2 = (a: number, b: number) => void
let f1: F1
let f2: F2 = f1
  1. 参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)
1
2
3
4
type F1 = (a: number) => void
type F2 = (a: number) => void
let f1: F1
let f2: F2 = f1

有一个特殊的情况

1
2
3
4
5
6
7
class Point { x: number; y: number }
class Point3D { x: number; y: number; z: number }
type F1 = (p: Point) => void
type F2 = (p: Point3D) => void
let f1: F1
let f2: F2 = f1 // 这里行得通
f1 = f2 // 这里会报错,因为f2的参数被认定为比f1多
  1. 返回值类型,只关注返回值类型本身即可
  • 如果是原始类型,需要两个类型相同
  • 如果是对象类型,则成员多的可以赋值给成员少的
1
2
3
4
5
6
7
8
9
10
11
// 返回值是原始类型
type F1 = () => string
type F2 = () => string
let f1: F1
let f2: F2 = f1
// 返回值是对象类型
type F3 = () => { a: number }
type F4 = () => { a: number; b: number }
let f3: F3
let f4: F4
f3 = f4

交叉类型

交叉类型 &:功能类似于接口继承 extends,用于组合多个类型为一个类型(常用于对象类型)

1
2
3
4
5
6
7
8
9
interface Person { name: string }
interface Detail { phone: string }
type PersonDetail = Person & Detail
let obj: PersonDetail = {
name: 'Alien',
phont: '10086'
}
// 相当于
type PersonDetail = { name: string; phone: string }

交叉类型(&)和接口继承(extends)的对比:

  • 相同点:都可以实现对象类型的组合。
  • 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 接口继承的情况
interface A {
fn: (value: number) => string
}
// B会报错,因为出现类型冲突了
interface B extends A {
fn: (value: string) => string
}

// 使用交叉类型
interface A {
fn: (value: number) => string
}
interface B {
fn: (value: string) => string
}
type C = A & B
// 不报错,而是将它们融合了,相当于
fn: (value: string | number) => string

泛型

泛型是可以在保证类型安全的前提下,让函数等与多种类型一起工作,实现复用

泛型函数

创建泛型函数

1
function callBack<Type>(value: Type): Type { return value }
  1. 语法:在函数名称后面加< >,添加类型变量
  2. Type 是类型变量,是一种特殊类型的变量,它能捕获用户提供的类型
  3. 在这里表示传入参数和返回值同类型
  4. 类型变量 Type,可以是任意合法的变量名称

调用泛型函数

1
console.log(callBack<number>(6));

image.png

  1. 语法:在函数名称的后面添加 < >,尖括号中指定具体的类型
  2. 传入的类型会被 Type 捕获,在这里就是指定类型为 number

简化调用泛型函数

image.png

image.png

可以利用类型推断机制,自动传入类型

泛型约束

默认情况下,Type 表示多个类型,导致可能无法访问任何属性,比如数组类型的 length 属性,因此需要泛型添加约束来收缩类型

  1. 指定更加具体的类型
1
2
3
4
function id<Type>(value: Type[]): Type[] {
console.log(value.length)
return value
}

将类型修改为 Type[],这样就可以访问 length 属性了

  1. 添加约束
1
2
3
4
5
interface ILength { length: number }
function id<Type extends ILength>(value: Type): Type {
console.log(value.length)
return value
}

创建描述约束的接口,通过 extends 关键字添加约束

传入的参数只要有指定的属性,就可以使用,属于类型兼容

也可以多个类型变量,也可以用类型变量约束类型变量

1
2
3
4
5
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')

多个类型变量之间用,逗号隔开

keyof 关键字接收一个对象类型,生成联合类型,在例子中 Type 获取了对象 person,所以使用 keyof 关键字之后相当于 'name' | 'age',就是说 Key 只能是键中的任意一个

泛型接口

接口也可以配合泛型来使用,以增加其灵活性,增强其复用性

1
2
3
4
5
6
7
8
interface IdFunc<Type> {
id: (value: Type) => Type
ids: () => Type[]
}
let obj: IdFunc<number> = {
id(value) { return value },
ids() { return [1, 3, 5] }
}
  1. 在接口名称后加<类型变量>,就是泛型接口
  2. 接口内的所有成员都可以使用类型变量
  3. 使用泛型接口时,需要显式指定具体的类型

泛型类

class 也可以配合泛型来使用

创建泛型类

1
2
3
4
5
6
7
class GenericNumber<NumType> {
defaultValue: NumType
add: (x: NumType, y: NumType) => NumType
}

const myNum = new GenericNumber<number>()
myNum.defaultValue = 10

类似泛型接口,在 class 名称后面添加 <类型变量>,这个类就变成了泛型类

泛型工具类型

泛型工具类型:TS 内置了一些常用的工具类型

  1. Partial<Type>用来构造一个类型,将 Type 的所有属性设置为可选
1
2
3
4
5
interface Props {
id: string
children: number[]
}
type PartialProps = Partial<Props>

构造出来的新类型 PartialProps 结构和 Props 相同,但是所有属性都变成可选的

  1. Readonly<Type>用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)
1
2
3
4
5
interface Props {
id: string
children: number[]
}
type ReadonlyProps = Readonly<Props>

构造出来的新类型 ReadonlyProps 结构和 Props 相同,但是所有属性都变成只读的

  1. Pick<Type, Keys>从 Type 中选择一组属性来构造新类型
1
2
3
4
5
6
interface Props {
id: string
title: string
children: number[]
}
type PickProps = Pick<Props, 'id' | 'title'>

从第一个类型变量 Type 表示选择哪个接口,第二个类型变量 Keys 表示选择该接口中的哪几个属性

  1. Record<Keys, Type>构造一个对象类型,属性键为 Keys,属性类型为 Type
1
2
3
4
5
6
type RecordObj = Record<'a' | 'b' | 'c', string[]>
let obj: RecordObj = {
a: ['1'],
b: ['2'],
c: ['3']
}

第一个参数表示对象有哪些属性,第二个参数表示对象属性的类型

例子中,新对象类型 RecordObj 有三个属性分别是 a/b/c,属性值的类型都是 string[]

索引签名类型

当无法确定对象中有哪些属性时,可以使用索引签名类型

1
2
3
4
5
6
7
interface AnyObject {
[key: string]: number
}
let obj: AnyObject = {
a: 1,
b: 2
}
  1. 使用[key: string]来约束接口允许出现的属性名称,要求只能是 string 类型的属性名称
  2. key 只是占用符,可以换成任何合法的变量名称

在 JS 中,数组是特殊的对象,特殊在数组的键是数字类型

1
2
3
4
interface MyArray<T> {
[n: number]: T
}
let arr: MyArray<number> = [1, 3, 5]

映射类型

映射类型:基于旧类型创建新类型(对象类型),减少重复、提高开发效率

1
2
3
4
type PropKeys = 'x' | 'y' | 'z'
type Type1 = { [Key in PropKeys]: number }
// 相当于
type Type1 = { x: number; y: number; z: number }

注意:映射类型只能在类型别名中使用,不能在接口中使用

1
2
3
4
type Props = { a: number; b: string; c: boolean }
type Type1 = { [Key in keyof Props]: number }
// 相当于
type Type1 = { a: number; b: number; c: number }

keyof Props可以获取对象类型 Props 中所有键的联合类型,即'a' | 'b' | 'c'

泛型工具类型也是通过映射类型实现的,比如Partial<Type>

1
2
3
type Partial<T> = {
[P in keyof T]?: T[P]
}

在后面加上问号可以使属性变为可选

索引查询类型T[P]在 TS 中叫做索引查询(访问)类型,用来查询属性的类型

1
2
type Props = { a: number; b: string; c: boolean}
type TypeA = Props['a'] // 相当于 type TypeA = number

Props['a'] 表示查询类型 Props 中属性 'a' 对应的类型 number。所以,TypeA 的类型为 number。

索引查询类型的其他使用方式:同时查询多个索引的类型

1
2
3
type Props = { a: number; b: string; c: boolean }
type TypeA = Props['a' | 'b'] // number | string
type TypeB = Props[keyof Props] // number | string | boolean