1. 高级类型
1.1 class
1.1.1 使用
TS全面支持 ES2015 中引入的 class 关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等)。
class 基本使用,如下:
class Person{}
const zs = new Person() // zs 的类型为 Person
解释:
-
根据 TS 中的类型推论,可以知道
Person类的实例对象zs的类型是Person -
TS 中的
class,不仅提供了class的语法功能,也作为一种类型存在
1.1.2 实例属性初始化
class Person {
name: string
age = 18
}
解释:
- 声明成员
name,类型为string(没有初始值) - 声明成员
age,并设置初始值,此时,可省略类型注解(TS 类型推论 为number类型)
1.1.3 构造函数
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
const zs = new Person('zs', 18) // zs 的类型为 Person
解释:
-
成员初始化(比如,
age: number)后,才可以通过this.age来访问实例成员。 -
需要为构造函数指定类型注解,否则会被隐式推断为
any -
构造函数不需要返回值类型
1.1.4 实例方法
class Point{
x = 10
y = 10
scale(n:number):void{
this.x *= n
this.y *= n
}
}
解释:方法的类型注解(参数和返回值)与函数用法相同
1.1.5 类继承
类继承的两种方式:
extends(继承父类)implements(实现接口)
说明:JS 中只有 extends,而 implements 是 TS 提供的
1. extends
class Father {
sayFather() {
console.log('father')
}
}
class Son extends Father{
saySon(){
console.log('son');
}
}
const zs = new Son()
解释:
-
通过
extends关键字实现继承 -
子类
Son继承父类Father,则Son的实例对象zs就同时具有了父类Father和 子类Son的所有属性和方法
2. implements
interface Father {
name: string
sayFather(): void
}
class Son implements Father {
name: 'zs'
sayFather(): number {
return 1
}
}
解释:
-
通过
implements关键字让class实现接口 -
Son类实现接口Father意味着,Son类中必须提供Father接口中指定的所有方法和属性
1.1.6 类成员可见性
1. 概念
可以使用 TS 来控制 class 的方法或属性对于 class 外的代码是否可见。
可见性修饰符包括:
public(公有的)protected(受保护的)private(私有的)
2. public
public:表示公有的、公开的,公有成员可以被任何地方访问,默认可见性。
class Father {
public name = 'father'
public sayHello(){
console.log('Hello')
}
}
解释:
- 在类属性或方法前面添加
public关键字,来修饰该属性或方法是共有的 - 因为
public是默认可见性,所以,可以直接省略
3. protected
protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见。
class Father {
name = 'father'
protected sayHello() {
console.log('Hello')
}
}
class Son extends Father {
age = 18
sayHi() {
console.log('Hi')
this.sayHello()
}
}
解释:
-
在类属性或方法前面添加
protected关键字,来修饰该属性或方法是受保护的 -
在子类的方法内部可以通过
this来访问父类中受保护的成员,但是,对实例不可见
4. private
private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的。
class Father {
name = 'father'
protected sayHello() {
console.log('Hello')
this.miss() // 只能在本类调用
}
private miss() {
console.log('除了自己所在的类,其他方式均无法调用')
}
}
解释:
-
在类属性或方法前面添加
private关键字,来修饰该属性或方法是私有的 -
私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的
5. readonly
除了可见性修饰符之外,还有一个常见修饰符就是:readonly(只读修饰符)。
readonly:表示只读,用来防止在构造函数之外对属性进行赋值。
class Person{
readonly age:number = 18
constructor (age:number){
this.age = age
}
}
解释:
-
使用
readonly关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法 -
注意:属性
age后面的类型注解(比如,此处的number)如果不加,则age的类型为 18 (字面量类型),这里说明readonly和 前面的const特性是一致的,单纯赋值不加类型,类型推断都是字面量 -
接口或者 {} 表示的对象类型,也可以使用
readonly
1.2 类型兼容性
1.2.1 两种类型系统
两种类型系统:
Structural Type System(结构化类型系统)Nominal Type System(标明类型系统)
TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型) ,类型检查关注的是值所具有的形状。也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。
class Point { x: number; y: number }
class Point2D { x: number; y: number }
const p: Point = new Point2D()
解释:
Point和Point2D是两个名称不同的类- 变量
p的类型被显示标注为Point类型,但是,它的值却是Point2D的实例,并且没有类型错误 - 因为 TS 是结构化类型系统,只检查
Point和Point2D的结构是否相同(相同,都具有 x 和 y 两个属性,属性类型也相同) - 如果在
Nominal Type System中(比如,C#、Java等),它们是不同的类,类型无法兼容
1.2.2 对象类型的兼容
注意:在结构化类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型,这种说法并不准确。更准确的说法:对于对象类型来说,y 的成员至少与 x 相同,则 x 兼容 y(成员多的可以赋值给少的)
class Point2D { x: number; y: number }
class Point3D { x: number; y: number; z: number }
const p: Point3D = new Point2D() // 报错
const p: Point2D = new Point3D() // 正常
解释:Point3D 的成员至少与 Point2D 相同,则 Point2D 兼容 Point3D。所以,成员多的 Point3D 可以赋值给成员少的 Point2D。
1.2.3 接口的兼容
接口之间的兼容性,类似于 class。并且,class 和 interface 之间也可以兼容。
interface 之间的兼容等同与 class:
interface Point2D { x: number; y: number }
interface Point3D { x: number; y: number; z: number }
let p1: Point3D = {
x: 1,
y: 2,
z: 3
}
let p2: Point2D = p1 // 正常
let p3: Point2D = {
x: 1,
y: 2
}
let p4:Point3D = p3 // 报错
interface 与 class 之间的兼容:
interface Point2D { x: number; y: number }
class Point3D { x: number; y: number; z: number }
let p1: Point2D = new Point3D()
1.2.4 函数的兼容
函数之间兼容性比较复杂,需要考虑:
- 参数个数
- 参数类型
- 返回值类型
1. 参数个数
参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)。函数中,如果参数传递的参数不够,那么后面的都默认为 undefined,但传递太多就直接出问题了。
let f1 = (a: number) => 0
let f2 = (a: number, b: number) => 0
f1 = f2 // 报错
f2 = f1 // 正常
解释:
- 参数少的可以赋值给参数多的,所以,
f1可以赋值给f2 - 在 JS 中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性
2. 参数类型
参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)。
简单类型的参数:
let f1 = (a: number) => 0
let f2 = (a: number) => 0
f1 = f2 // 正常
let f3 = (a: number) => 0
let f4 = (a: string) => 0
f3 = f4 // 报错
f4 = f3 // 报错
解释:
- 相同位置函数参数类型相同,是可以兼容的
- 相同位置函数参数类型不同,不能兼容
- 这里的参数位置指两边相同的位置都有参数
复杂类型的参数:
interface Point2D { x: number; y: number }
interface Point3D { x: number; y: number; z: number }
let f2 = (p: Point2D) => 0
let f3 = (p: Point3D) => 0
f2 = f3 // 报错
f3 = f2 // 正常
解释:
- 注意,此处与前面讲到的接口兼容性冲突
- 技巧:将对象拆开,把每个属性看做一个个参数,则参数少的
f2可以赋值给参数多的f3
3. 返回值类型
返回值类型,只关注返回值类型本身即可:
简单类型的参数:
type F3 = () => string
type F4 = () => string
let f3: F3 = () => { return 'hello' }
let f4: F4 = f3 // 正常
type F5 = () => string
type F6 = () => number
let f5: F5 = () => { return 'hello' }
let f6: F6 = f5 // 报错
如果返回值类型是原始类型,此时两个类型要相同
复杂类型的参数:
type F7 = () => { name: string }
type F8 = () => { name: string, age: number }
let f7: F7 = () => { return { name: "zs" } }
let f8: F8 = f7 // 报错(成员少的不能给成员多的)
let f88: F8 = () => { return { name: "zs", age: 18 } }
let f77: F7 = f88 // 正常(成员多的可以给成员少的)
如果返回值类型是对象类型,此时成员多的可以赋值给成员少的(对象类型兼容的特点)
1.3 交叉类型
1.3.1 用法
交叉类型(&):功能类似于接口继承 extends,用于组合多个类型为一个类型(常用于对象类型)。
比如,
interface Person { name: string }
interface Person2 { phone: number }
type Person3 = Person & Person2
let obj: Person3 = {
name: 'zs',
phone: 123
}
解释:使用交叉类型后,新的类型 Person3 就同时具备了 Person 和 Person2 的所有属性类型。
相当于:
type Person3 = { name: string; phone: number }
1.3.2 交叉类型与接口继承的不同点
交叉类型(&) 和接口继承(extends) 的对比:
-
相同点:都可以实现对象类型的组合
-
不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同
接口继承同名属性(基本数据类型):
interface Person{name: string}
interface Person2 extends Person{
name: number // 报错
}
交叉类型同名属性(基本数据类型):
interface Person { uname: string }
interface Person2 { uname: number }
type Person3 = Person & Person2
let obj: Person3 = {
uname: 1 // 报错
}
报错原因是:uname 既可以是 string 和 number 类型,这种混合的类型在类型机制不存在,所以为 never 类
接口继承同名属性(函数形参):
interface A{
fn :(val: number) => string
}
interface B extends A{
fn :(val: string) => string // 报错
}
交叉类型同名属性(函数形参):
interface A {
fn: (val: number) => string
}
interface B {
fn: (val: number) => string
}
type C = A & B
let obj: C = {
fn(val: number | string) {
return ''
}
} // 正常
1.4 泛型
1.4.1 概述
泛型是可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数、接口、class中。
需求:创建一个 id 函数,传入什么数据就返回该数据本身(也就是说,参数和返回值类型相同)
比如,id(10) 调用以上函数就会直接返回 10 本身。但是,该函数只接收数值类型,无法用于其他类型。为了能让函数能够接受任意类型,可以将参数类型修改为 any。但是,这样就失去了 TS 的类型保护,类型不安全。
泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用。
实际上,在 C#和 Java 等编程语言中,泛型都是用来实现可复用组件功能的主要工具之一。
1.4.2 创建泛型函数
function id<Type>(val: Type): Type {
return val
}
解释:
-
语法:在函数名称的后面添加
<>,尖括号中添加类型变量,比如此处的Type -
类型变量
Type,是一种特殊类型的变量,它处理类型而不是值 -
该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)
-
因为
Type是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型 -
类型变量
Type,可以是任意合法的变量名称
1.4.3 调用泛型函数
function id<Type>(val: Type): Type {
return val
}
let num = id<number>(1) // number 类型
let str = id<string>('hello') // string 类型
解释:
- 语法:在函数名称的后面添加
<>,尖括号中指定具体的类型,比如,此处的number - 当传入类型
number后,这个类型就会被函数声明时指定的类型变量Type捕获到 - 此时,
Type的类型就是number,所以,函数 id 参数和返回值的类型也都是number
同样,如果传入类型 string,函数 id 参数和返回值的类型就都是 string。
这样,通过泛型就做到了让 id 函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全。
1.4.4 简化调用泛型函数
未简化:
let num = id<number>(1) // number 类型
let str = id<string>('hello') // string 类型
简化:
let num = id(1) // number 类型
let str = id('hello') // string 类型
解释:
- 在调用泛型函数时,可以省略 <类型> 来简化泛型函数的调用
- 此时,TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量
Type的类型 - 比如,传入实参 1,TS 会自动推断出变量
num的类型number,并作为Type的类型
推荐:使用这种简化的方式调用泛型函数,使代码更短,更易于阅读。
说明:当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数。
1.4.5 泛型约束
泛型约束:默认情况下,泛型函数的类型变量 Type 可以代表多个类型,这导致无法访问任何属性。
比如,id('a') 调用函数时获取参数的长度:
function id<Type>(val: Type): Type{
console.log(val.length) // 报错
return val
}
解释:Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length
此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)
添加泛型约束收缩类型,主要有以下两种方式:
- 指定更加具体的类型
- 添加约束
1. 指定更加具体的类型
比如,将类型修改为 Type[](Type 类型的数组),因为只要是数组就一定存在 length 属性,因此就可以访问了。
function id<Type>(val: Type[]): Type[] {
console.log(val.length) // 报错
return val
}
2. 添加约束
interface Ilength { length: number }
function id<Type extends Ilength>(val: Type): Type {
console.log(val.length)
return val
}
解释:
- 创建描述约束的接口
ILength,该接口要求提供length属性 - 通过
extends关键字使用该接口,为泛型(类型变量)添加约束 - 该约束表示:传入的类型必须具有
length属性
注意:传入的实参(比如,数组)只要有 length 属性即可,这也符合前面讲到的接口的类型兼容性
1.4.6 多个泛型变量
泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)。
比如,创建一个函数来获取对象中属性的值:
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key): void {
console.log(obj[key])
}
let person = {
name: 'zs',
age: 18
}
getProp(person, 'name') // zs
getProp(person, 'age') // 18
// getProp(person, 'phone') // 报错
getProp([1, 'jack', 'abc'], 2) // abc
getProp('abc', 'toUpperCase') // [Function: toUpperCase]
解释:
- 添加了第二个类型变量
Key,两个类型变量之间使用,逗号分隔 keyof关键字接收一个对象类型,生成其所有键的联合类型- 本示例中前面的
keyof Type实际上获取的是person对象所有键的联合类型,也就是:'name' | 'age' - 本示例中后面的
keyof Type实际上获取的是js对象或基础数据类型的所有键的联合类型,也就是原本就能使用的方法,且第二个参数为数字时,代表索引 - 类型变量
Key受Type约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性
1.4.7 泛型接口
1. 使用
泛型接口:接口也可以配合泛型来使用,以增加其灵活性,增强其复用性。
interface IdFunc<Type> {
id: (val: Type) => Type
ids: () => Type[]
}
let obj: IdFunc<number> = {
id(val) { return val },
ids() { return [1, 2, 3] }
}
解释:
- 在接口名称的后面添加
<类型变量>,那么,这个接口就变成了泛型接口 - 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量
- 使用泛型接口时,需要显式指定具体的类型(比如,此处的
IdFunc<nunber>) - 此时,
id方法的参数和返回值类型都是number;ids方法的返回值类型是number[]
2. 数组在 TS 中就是一个泛型接口
实际上,JS 中的数组在 TS 中就是一个泛型接口。
et num = [1, 2, 3] // num 是 num[] 类型
let str = ['a', 'b', 'c'] // str 是 string[] 类型
let x = [1, 'a', true] // x 是 (string | numbner | boolean)[] 类型
解释:当我们在使用数组时,TS 会根据数组的不同类型,来自动将类型变量设置为相应的类型
1.4.8 泛型类
class 也可以配合泛型来使用。
1. 创建实例必须指定明确的类型
创建泛型类:
class Car<Type>{
num: Type
add: (x: Type, y: Type) => Type
}
const myCar = new Car<number>() // 创建实例必须指定明确的类型
myCar.num = 12345
解释:
- 类似于泛型接口,在
class名称后面添加 <类型变量>,这个类就变成了泛型类 - 此处的
add方法,采用的是箭头函数形式的类型书写方式
类似于泛型接口,在创建 class 实例时,在类名后面通过 <类型> 来指定明确的类型。
2. 创建实例不用指定明确的类型
class Car<Type>{
num: Type
add: (x: Type, y: Type) => Type
constructor(num: Type){
this.num = num
}
}
const myCar = new Car<number>(12345)
通过构造函数和类型推断可以实现创建实例的时候,不用指定明确的类型,具体类型根据创建实例所传入的值决定。
1.4.9 泛型工具类型
1. 简介
泛型工具类型:TS 内置了一些常用的工具类型,来简化 TS 中的一些常见操作。
说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。
这些工具类型有很多,主要学习以下几个:
-
Partial<Type> -
Readonly<Type> -
Pick<Type, Keys> -
Record<Keys, Type>
2. Partial<Type>
泛型工具类型 - Partial<Type> 用来构造(创建)一个类型,将 Type 的所有属性设置为可选。
interface Props {
id: number
arg: number[]
}
type P = Partial<Props>
解释:构造出来的新类型 PartialProps 结构和 Props 相同,但所有属性都变为可选的。
3. Readonly<Type>
泛型工具类型 - Readonly<Type> 用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)。
interface Props2 {
id: number
arg: number[]
}
type P2 = Readonly<Props2>
let p2: P2 = { id: 1, arg: [1, 2, 3] }
p2.id = 2 // 报错
解释:构造出来的新类型 P2 结构和 Props2 相同,但所有属性都变为只读的。
当我们想重新给 id 属性赋值时,就会报错:无法分配到 "id" ,因为它是只读属性。
4. Pick<Type>
泛型工具类型 - Pick<Type, Keys> 从 Type 中选择一组属性来构造新类型。
interface Props3 {
id: number
arg: number[]
arg2: string[]
}
type P3 = Pick<Props3, 'id' | 'arg2'>
解释:
Pick工具类型有两个类型变量:- 表示选择谁的属性
- 表示选择哪几个属性
- 其中第二个类型变量,如果只选择一个则只传入该属性名即可
- 第二个类型变量传入的属性只能是第一个类型变量中存在的属性
- 构造出来的新类型
P3,只有id和arg2两个属性类型
5. Record<Type>
4.4 泛型
泛型工具类型 - Record<Keys,Type> 构造一个对象类型,属性键为 Keys,属性类型为 Type。
type P4 = Record<'num1' | 'num2' | 'num3', number[]>
解释:
Record工具类型有两个类型变量:- 表示对象有哪些属性
- 表示对象属性的类型
- 构建的新对象类型
P4表示:这个对象有三个属性分别为:num1、num2、num3,属性值的类型都是number[]
1.5 索引签名类型
1.5.1 基本使用
绝大多数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型。
使用场景:当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时,就用到索引签名类型了。
interface Person {
[key: string]: number
}
let obj: Person = {
age: 1,
height: 111
}
解释:
- 使用
[key: string]来约束该接口中允许出现的属性名称。表示只要是string类型的属性名称,都可以出现在对象中 - 这样,对象
obj中就可以出现任意多个属性(比如:age、height等) key只是一个占位符,可以换成任意合法的变量名称- 隐藏的前置知识:JS 中对象
({})的键是string类型的
1.5.2 和泛型一起使用
在 JS 中数组是一类特殊的对象,特殊在数组的键(索引)是数值类型。
并且,数组也可以出现任意多个元素。所以,在数组对应的泛型接口中,也用到了索引签名类型。
interface MyArray<T> {
[key: number]: T
}
let obj2: MyArray<number> = [1, 3, 5]
console.log(obj2[1]) // 3
解释:
MyArray接口模拟原生的数组接口,并使用[n: number]来作为索引签名类型- 该索引签名类型表示:只要是
number类型的键(索引)都可以出现在数组中,或者说数组中可以有任意多个元素 - 同时也符合数组索引是
number类型这一前提
1.6 映射类型
1.6.1 联合类型和映射类型
映射类型:基于旧类型创建新类型(对象类型),减少重复、提升开发效率。
比如,类型 A 有 x、y、z 三个属性,另一个类型 B 中也有这三个属性,并且类型 B 中这三个属性类型相同:
type A = 'x' | 'y' | 'z'
type B = {
x: number
y: number
z: number
}
这样书写没错,但 x、y、z 重复书写了两次。像这种情况,就可以使用映射类型来进行简化。
type A = 'x' | 'y' | 'z'
type C = { [key in A]: number }
解释:
- 映射类型是基于索引签名类型的,所以,该语法类似于索引签名类型,也使用了
[] Key in A表示Key可以是 A 联合类型中的任意一个- 注意:映射类型只能在类型别名中使用,不能在接口中使用
1.6.2 对象和映射类型
映射类型除了根据联合类型创建新类型外,还可以根据对象类型来创建:
type o = {
name: string,
age: number,
weight: number
}
type B = { [key in keyof o]: number }
解释:
- 首先,先执行
keyof o获取到对象类型o中所有键的联合类型即,'name' | 'age' | 'weight' - 然后,
key in ...就表示key可以是对象o中所有的键名称中的任意一个 - 注意如示例这样指定会使新类型
B的所有属性是同一个类型
1.6.3 泛型工具与接口
快速生成属性值相同的类型,我们除了映射类型之外,也可以想到刚刚说过的泛型工具。
实际上,前面讲到的泛型工具类型(比如 Partial<Type>)都是基于映射类型实现的。
Partial<Type> 的实现:
// 实现: 这里的 Type 是个对象
type Partical<Type> = {
[key in keyof Type]?: Type[key]
}
interface Props {
id: number
arg: number[]
}
type P = Partial<Props>
解释:
keyof Type即keyof Props表示获取Props的所有键,也就是:'id' | 'arg'- 在
[]后面添加?,表示将这些属性变为可选的,以此来实现Partial的功能 - 冒号后面的
Type[key]表示获取Type中每个键对应的类型。比如,如果是id则类型是number;如果是arg则类型是number[] - 最终,新类型
P和旧类型Props结构完全相同,只是让所有类型都变为可选了
1.6.4 索引查询
刚刚用到的 Type[P] 语法,在 TS 中叫做索引查询(访问)类型。
作用:用来查询属性的类型。
type Props = {
a: number,
b: string,
c: boolean
}
type A = Props['a'] // number
type B = Props['b'] // string
type C = Props['c'] // boolean
type D = Props['d'] // 报错
解释:Props['a'] 表示查询类型 Props 中属性 'a' 对应的类型 number。所以,A 的类型为 number
注意:[] 中的属性必须存在于被查询类型中,否则就会报错。
1.6.5 多索引查询
索引查询类型的其他使用方式:同时查询多个索引的类型。
type Props = {
a: number,
b: string,
c: boolean
}
type A = Props['a' | 'b'] // string | number
type B = Props[keyof Props] // string | number | boolean
解释:
- 使用字符串字面量的联合类型,获取属性
a和b对应的类型,结果为:string | number - 使用
keyof操作符获取Props中所有键对应的类型,结果为:string | number | boolean
如有错误,敬请指正,欢迎交流🤝,谢谢♪(・ω・)ノ