目录
第三章、TypeScript的数据类型
3.1 TypeScript的高级类型
3.1.1 class
3.1.1.1 熟悉class类
- 引言
class Person {}
const p = new Person()
当创建一个Person类时,添加了实例对象p,由类型推论可以知道它的类型是Person;说明TS 中的 class,不仅提供了 class 的语法功能,也作为一种类型存在。接下来跟着小编一起理解class吧。
- 初始化属性
class Person {
age: number // 适用没有默认值
name = 'VE' // 适用于有默认值
}
- 声明成员 age,类型为 number(没有初始值)。
- 声明成员 name,并设置初始值,此时,可省略类型注解(TS 类型推论 为 string 类型)。
- 当没有默认赋值时,需要自定义属性的类型,否则会默认为any类型;赋值后,属性的类型会与赋值的类型一致,赋值的类型ts会类型推论。
- 看下图的几种情况:
-
构造函数
class Person {
age: number
name: string
constructor (name: string, age: number){ // 初始化实例对象使用的,写了contructor函数后初始化实例对象时也需要传参从而初始化实例中的属性
this.age = age
this.name = name
}
}
const p = new Person('10', 10)
- 只有初始化(比如,age: number)后,才可以通过 this.age 来访问实例成员。
- 需要为构造函数指定类型注解,否则会被隐式推断为 any;构造函数不需要返回值类型。
- constructor构造函数是初始化实例对象使用的,写了contructor函数后初始化实例对象时也需要通过传参从而初始化实例中的属性,参数顺序跟constructor一致
-
实例方法
class Point {
x: number
y: number
scale (n: number): void{
this.x *= n
this.y *= n
}
}
-
方法初始化,它的的类型注解(参数和返回值)与函数用法相同
3.1.1.2 class类继承的两种方式
类继承的两种方式:1、extends(继承父类) 2、implements(实现接口)。
注意:JS 中只有 extends,而 implements 是 TS 提供的。
- extends(继承父类)
class Animal { // 这是父类Animal
distance: string = '10' // 父类中的属性
move () { // 父类中的方法
console.log('move along')
}
}
class Dog extends Animal { // 子类 Dog 继承父类 Anima
bark () { // 子类自身的方法
console.log('汪汪汪~')
}
}
const dog = new Dog() // dog是Dog的实例对象,
// 从而dog同时具有了父类 Animal 和 子类 Dog 的所有属性和方法
dog.move()
dog.bark()
dog.distance = '20'
console.log('距离', dog.distance)
- 通过 extends 关键字实现继承。
- 子类 Dog 继承父类 Animal,则 Dog 的实例对象 dog 就同时具有了父类 Animal 和 子类 Dog 的所有属性和方法。
-
implements(实现接口): 这是ts新增的,顾名思义就是对一个接口的实现
interface Singable { // 这是添加的一个接口,定义了Singable有哪些属性和方法的类型
name: string
sing: () => void
toSing(): void
}
class Person implements Singable { // Person类是对Singable接口的实现
name = 'VE'
sing = () => { // 函数方面是箭头函数还是普通函数需要我们自己规范
console.log('你是我的小啊小苹果')
}
toSing(): void {
console.log('去ktv唱歌')
}
}
const p = new Person() // p是Person的一个实例对象
// 从而拥有了Singable 接口 指定的所有方法和属性
console.log(p.name)
p.sing()
p.toSing()
- 通过 implements 关键字让 class 实现接口。
- Person 类实现接口 Singable 意味着,Person 类中必须提供 Singable 接口中指定的所有方法和属性。
3.1.1.3 class类的5种修饰符
可见性修饰符包括:1、public(公有的) 2、protected(受保护的) 3、private(私有的)4、readonly(只读修饰符)5、static(静态的)
-
public(公有的)
class Animal {
public move () { // 在父类的方法中添加pubilc(默认,可省略)
console.log('move along')
}
}
class Dog extends Animal { // 子类
bark () {
console.log('汪汪汪~')
}
}
class Cat extends Animal { // 子类
bark () {
console.log('喵~喵~喵~')
}
}
const dog = new Dog() // 由于父类设置的为pubilc修饰符,所有其子类实例可以调用父类的方法
dog.move()
dog.bark()
const cat = new Cat() // 由于父类设置的为pubilc修饰符,所有其子类实例可以调用父类的方法
cat.move()
cat.bark()
- 在类属性或方法前面添加 public 关键字,来修饰该属性或方法是共有的。
- 因为 public 是默认可见性,所以,可以直接省略。
-
protected(受保护的)
class Animal {
protected move () { // 将方法添加protected关键字设置成受保护的,
console.log('move along')
}
toMove () {
this.move() // 在父类本身中只能通过this访问受保护的属性/方法
}
}
class Dog extends Animal {
bark () {
console.log('汪汪汪~')
this.move() // 在子类中也只能通过this访问受保护的属性/方法
}
}
// const animal = new Animal() // 但是他们的实例不能访问到受保护的属性和方法
// animal.move() // 本身实例调用会报错属性move受保护,只能在类“Animal”及其子类中访问
const dog = new Dog()
// dog.move() // 子类调用会报错属性move受保护,只能在类“Animal”及其子类中访问
dog.bark() // 可以通过提供方法添加新的方法从而间接访问
console.log('=====')
dog.toMove()
- 在类属性或方法前面添加 protected 关键字,来修饰该属性或方法是受保护的。
- 在子类的方法内部可以通过 this 来访问父类中受保护的成员,但是,对其声明所在类和子类不可见!
-
private(私有的)
class Animal {
private move () {
console.log('move along') // 将方法添加private 关键字设置成私有的,
}
toMove () {
this.move() // 只能在当前类中访问
}
}
class Dog extends Animal {
bark () {
console.log('汪汪汪~')
// this.move() // 在子类的方法中也不能访问
}
}
// const animal = new Animal()
// animal.move() // 本身实例调用会报错属性move斯私有属性,只能在类“Animal”中访问
const dog = new Dog()
// dog.move() // 子类调用会报错属性move为私用属性,只能在类“Animal”中访问
dog.bark()
console.log('=====')
dog.toMove() // 但是子类可以通过调用父类提供的方法从而间接的访问父类的私有属性
- 在类属性或方法前面添加 private 关键字,来修饰该属性或方法是私有的。
- 私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的!
-
readonly(只读修饰符)
class Animal {
readonly species: string = '爬行动物' // 修饰属性为只读是, 仅可在构造函数中修改
move () {
// this.species = '飞行动物' // 报错无法赋值,species为只读属性
console.log('move along')
}
constructor(species: string){
this.species = species
}
}
class Dog extends Animal {
bark () {
console.log('汪汪汪~')
}
}
const dog = new Dog('爬行')
dog.bark()
console.log('dog', dog.species)
- 使用 readonly 关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法。
- 注意:只要是readonly来修是属性,必须手动提供明确的类型,属性 species 后面的类型注解(比如,此处的 string)如果不加,则 species 的类型为 爬行动物 (字面量类型)。
- 仅可在构造函数中修改(也就是初始化时)
- 接口或者 {} 表示的对象类型,也可以使用 readonly
// 接口或者 {} 表示的对象类型,也可以使用 readonly
interface IPerson {
readonly name: string
}
let p: IPerson = {
name: 'VE'
}
let per: { readonly name: string, age: number } = {
name: 'VE',
age: 18
}
// p.name = 'LO' // 如果没加readonly是可以修改的,但是加了之后会报错为只读属性,无法赋值
// per.name = 'LO'
per.age = 20
- static(静态的)
class Animal {
// 普通属性
name: string
// 静态属性
static gander: string = '女'
// 构造函数前不能添加 static
constructor (name: string) {
this.name = name
// 访问静态属性,需要通过 【类名.属性】访问
console.log(this.name, Animal.gander)
}
// 普通方法
sayHi () {
console.log('hello你们好')
}
// 静态方法
static sayStatic () {
console.log(Animal.gander)
}
}
// 实例化对象
const per = new Animal('花花')
// 访问普通方法,通过实例
per.sayHi()
// 访问静态方法,要通过 【类名.方法】访问
Animal.sayStatic()
- 使用 static 关键字修饰该属性/方法是静态的
- 静态成员 在使用时通过 类名.静态成员 这种语法来调用的,而不是通过 this实例
3.1.2 类型兼容
- 目前有两种类型系统:1 Structural Type System(结构化类型系统) 2 Nominal Type System(标明类型系统)
- TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型),类型检查关注的是值所具有的形状。也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。(可以这么理解但是,该说法并不准确)。用于确定一个类型是否能赋值给其他类型
- 对于对象类型来说,y 的成员至少与 x 相同,则 x 兼容 y(成员多的可以赋值给少的)
- 例子1:
class Point {number1 = 10; number2 = 9}
class Point2D {number1 = 16; number2 = 12}
const p: Point = new Point2D()
console.log('p', p.number1, p.number2)
- Point 和 Point2D 是两个名称不同的类。
- 变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并且没有类型错误。
- 因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(结果是相同,都具有 x 和 y 两个属性,属性类型也相同)。
- 但是,如果在 Nominal Type System(标明类型系统) 中(比如,C#、Java 等),它们是不同的类,类型无法兼容。
- 这里我们可以理解p被类型标注了Point的number1与number2,分别都是number类型,最后被赋值了Point2D
- 例子2:兼容可以立即成向下兼容,在class中定义:成员多的可以赋值给成员少的(理解,左边的参数必须都有,而右边赋值的则是至少需要包含左边参数)
class Point {number1 = 10; number2 = 9}
class Point2D {number1 = 16; number2 = 12}
class Point3D {number1 = 17; number2= 1; numbe3 = 20 }
const p: Point = new Point2D()
const p1: Point2D = new Point3D() // 成员多的 Point3D 可以赋值给成员少的Point2D / Point
console.log('p', p.number1, p.number2)
……
3.1.3 交叉类型
-
交叉类型(&):功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)
interface Person { // 第一个接口定义了一部分类型
name: string
}
interface Contact { // 第二个接口定义了不部分类型
phone: string
}
type PersonDetail = Person & Contact // 定义新的类型利用&交叉类型合并, 注hengsm意新的类型用tpye类型别名声明的
let person: PersonDetail = { // 新的类型同时拥有上面的两种声明类型
name: 'zs',
phone: '177 5490 0987'
}
- 使用交叉类型后,新的类型 PersonDetail 就同时具备了 Person 和 Contact 的所有属性类型。
- 相当于type PersonDetail = { name: string, phone: string }
-
交叉类型(&)和接口继承(extends)的对比
-- 使用extends
interface A {
name: string
fn: (number: number) => void
}
interface B extends A {
gander: string
// fn: (number: string) => void
}
const obj: B = {
name: 'VE',
fn: (number: number) => {},
gander: '女'
}
-- 使用交叉类型
interface A {
name: string
fn: (number: number) => number
}
interface B {
// name: number
fn: (number: string) => string
}
type C = B & A
const obj: C = {
// name,
name: '',
fn: (number) => {
return number
}
}
-- 相同点:都可以实现对象类型的组合。
-- 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。
extends 不支持相同属性/方法的继承,会报错类型不兼容
& 支持相同属性/方法的继承,可以理解成利用联合类型 & 连接,两种类型同时具备才可,否则会报错,不可分配
3.1.4 泛型
-
泛型是可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数、接口、class 中。
3.1.4.1 创建泛型函数
-
引入思考:目前有一个需求,创建一个 id 函数,传入什么数据就返回该数据本身(也就是说,参数和返回值类型相同)
// 实现方法一:手动限制一个类型,缺点也很明显,我们只能传number数据类型,传其他数据类型时会报错
function id_first(value: number) {
return value
}
// 实现方法二:不限制数据类型,那么ts本身类型推断,则会将传参推断为any,失去了ts的意义
function id_second(value) {
return value
}
- 实现方法:创建泛型函数,泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用。
// 实现方法:创建泛型函数,泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用。
function id<Type>(value: Type): Type {
return value
}
- 语法:在函数名称的后面添加 <>(尖括号),尖括号中添加类型变量(可自定义语义化名称),比如此处的 Type。
- 该函数中类型变量 Type,是一种特殊类型的变量,它处理类型而不是值。
- 该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)。
- 因为 Type 是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型。
- 类型变量 Type,可以是任意合法的变量名称
3.1.4.2 泛型函数的调用
- 正常调用
function id<Type>(value: Type): Type {
return value
}
// 调用泛型函数:既要传类型,也要传参数
const num = id<number>(1)
const str = id<string>('10')
const arr = id<(number | string)[]>(['10', '1', 10])
- 语法:在函数名称的后面添加 <>(尖括号),尖括号中指定传参的具体的类型,比如,此处的 number。
- 当传入类型 number 后,这个类型就会被函数声明时指定的类型变量 Type 捕获到。
- 此时,Type 的类型就是 number,所以,函数 id 参数和返回值的类型也都是 number
- 简化调用
function id<Type>(value: Type): Type {
return value
}
// <类型> 可以不写
let sp_num = id(10)
let sp_str = id('10')
let sp_arr = id(['10', '1', 10])
主要注意:这种方法是利用类型推断实现的,直接传参要与前面带类型的要区分
- 例如sp_num、sp_str变量,我们能发现函数被推断成id<10>(value: 10): 10 字面量类型,最终返回数字10才使得sp_num推断成number类型
- 例如sp_arr变量,函数被推断<(string | number)[]>(value: (string | number)[]): (string | number)[],最终返回结果['10', '1', 10]使得sp_arr推断成(string | number)[]类型
- 如果我们期望第1点最终是number类型而不是字面量类型,但是类型推断又是给我们推断成字面量类型,我们还是需要传类型的:id<number>(10)
3.1.4.3 泛型约束
-
默认情况下,泛型函数的类型变量 Type 可以代表多个类型,这导致无法访问任何属性,如下:
function id_view<Type>(value: Type): Type {
console.log('长度', value.length) // 报错原因:Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型没有 length。
return value
}
-
于是,就需要为泛型添加约束来收缩类型(缩窄类型取值范围),添加泛型约束收缩类型,主要有以下两种方式:1、指定更加具体的类型 2、添加约束
- 1、指定更加具体的类型
例如:将类型修改为 Type[](Type 类型的数组),因为只要是数组就一定存在 length 属性,因此就可以访问了
function id_view<Type>(value: Type[]): Type[] { // 将类型修改为 Type[](Type 类型的数组),因为只要是数组就一定存在 length 属性,因此就可以访问了
console.log('长度', value.length)
return value
}
id_view([1, 2])
id_view<number | string>([1, '2'])
id_view<number[]>([1, 2]) // 注意添加<类型>是的写法,由于我们已经在类型中写了Type[],所以使用时不需要再加[]
注意书写:(类型可不传,ts会通过类型断言得到)
报错:
- 2、添加约束
interface ILength { length: number } // 创建描述约束的接口 ILength,提供length属性
function id_view<Type extends ILength>(value: Type): Type { // 对Type添加约束,通过 extends 关键字使用ILength接口
console.log('长度', value.length)
return value
}
// 使用类型约束之后我们在调用时就必须需要传具有 length 属性的类型(string/array…),否则调用会报错
// id_view<number>(10) // 会报错number类型不满足约束的ILength
id_view('10')
id_view<string>('10')
// id_view<{name: string, age: number}>({name: 'VE', age: 23}) // 会报错
- 创建描述约束的接口 ILength,该接口要求提供 length 属性。
- 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。
- 该约束表示:传入的类型必须具有 length 属性
-
泛型的类型变量可以有多个,并且类型变量之间还可以约束
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) { // keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型
return obj[key]
}
let person = {name: 'VE', age: 23}
const personName = getProp(person , 'name')
console.log('personName', personName)
- 添加了第二个类型变量 Key,两个类型变量之间使用(,)逗号分隔。
- keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。
- 本示例中 keyof Type 实际上获取的是 person 对象所有键的联合类型,也就是:'name' | 'age'。
- 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性
3.1.4.4 泛型接口
-
接口配合泛型使用,以增加其灵活性,复用性
interface idFunc<Type> {
id: (vlue: Type) => Type
ids: () => Type[]
}
const obj: idFunc<number> = { // 使用泛型接口时,必须需要显式指定具体的类型
id(num) {
return num
},
ids() {
return [1, 2]
}
}
- 语法:在接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口。
- 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量。
- 使用泛型接口时,需要显式指定具体的类型(比如,此处的 IdFunc<number>)。
- 此时,id 方法的参数和返回值类型都是 number;ids 方法的返回值类型是 number[]。
-
JS 中的数组在 TS 中就是一个泛型接口:当我们在使用数组的方法时,TS 会根据数组的不同类型,来自动将类型变量设置为相应的类型,如下:
const strsArr = ['a', 'b', 'c']
const arr1 = [1, 2, 'a']
strsArr.sort()
arr1.sort()
arr1.forEach(()=>{})
3.1.4.5 泛型类
-
使用class类 配合泛型来使用
-
创建泛型类并使用
关于创造泛型类涉及到的报错:
class GenericNumber<NumType> { // 创建泛型类
defaultValue: NumType
add?: ( x: NumType, y: NumType ) => NumType
constructor(defaultValue: NumType) { // 创建构造函数
this.defaultValue = defaultValue
}
}
const myNum = new GenericNumber<number>(10) // 类似于泛型接口,在创建 class 实例时,在类名后面通过 <类型> 来指定明确的类型。
myNum.defaultValue = 10
// 初始化方法
myNum.add = function(a, b) {
return a + b
}
// 之后调用方法
const value = myNum.add(1, 2)
console.log(myNum.defaultValue, value)
(针对于泛型类里的方法创造了实例之后一定要先初始化再调用)
3.1.4.6 泛型工具类型
-
TS 内置了一些常用的工具类型,来简化 TS 中的一些常见操作,它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。
-
常用的:1、Partial<Type> 2、 Readonly<Type> 3、 Pick<Type, Keys> 4 、Record<Keys, Type>
- 1、Partial<Type>:用来构造(创建)一个类型,将 Type 的所有属性设置为可选
interface Props {
id: string
children: number[]
}
// 构造出来的新类型 PartialProps 结构和 Props 相同,但所有属性都变为可选的。鼠标悬浮在PartialProps上可以发现PartialProps的属性都添加了?:表示可有可无
type PartialProps = Partial<Props>
let partialVal: PartialProps = {} // 添加Partial后,参数可写可不写
let noPartialVal: Props = { // 未添加Partial,使用该类型字段必须都传
id: '1',
children: [1, 2]
}
- 2、Readonly<Type> 用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)
interface Props {
id: string
children: number[]
}
type readonlyProps = Readonly<Props> // 会给每一个类型前面添加一个 readonly 修饰
let readonVal: readonlyProps = { // 一旦定义里面的属性就无法修改
id: '001',
children: [1, 2, 3]
}
// readonVal.id = '002' // 无法修改,报错无法赋值,为只读属性
- 3、Pick<Type, Keys> 从 Type 中选择一组已有的属性来构造新类型
interface Props {
id: string
children: number[]
}
type PickProps = Pick<Props, 'id'>
let PickVal: PickProps = {
id: '',
}
// 如果传多个使用 | 连接,选择的不能是类型中没有的属性
type PickProps = Pick<Props, 'id' | 'children'>
let PickVal: PickProps = {
id: '',
children: [1, 2, 3]
}
- 4、Record<Keys,Type> 构造一个对象类型,属性键为 Keys ,使用 | 连接多个属性,属性类型为 Type。
interface Props {
id: string
children: number[]
}
type RecordProps = Record<'id', Props>
let RecordVal: RecordProps = {
id: {
id: '',
children: [1, 2]
}
}
type RecordProps2 = Record<'id' | 'title', string>
let RecordVal2: RecordProps2 = {
id: '',
title: ''
}
3.1.5 索引签名类型
- 绝大多数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型。
- 使用场景:当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时,就用到索引签名类型了
interface AnyObject {
[key: string]: number
}
let obj: AnyObject = {
a: 1,
b: 22,
1: 2
}
- 使用 [key: string] 来约束该接口中允许出现的属性名称。表示只要是 string 类型的属性名称,都可以出现在对象中。
- 这样,对象 obj 中就可以出现任意多个属性(比如,a、b 等)。
- key 只是一个占位符,可以换成任意合法的变量名称。
- 隐藏的前置知识:JS 中对象({})的键是 string 类型的。
interface MyArray<T> {
[key: number]: T // 索引都是数字,数组符合要求,T规定类型
}
let arr: MyArray<number> = [1, 2, 3]
- 在 JS 中数组是一类特殊的对象,特殊在数组的键(索引)是数值类型。
- 并且,数组也可以出现任意多个元素。所以,在数组对应的泛型接口中,也用到了索引签名类型
3.1.6 映射类型
小编重开一篇文章单独讲。