TS速成day3

235 阅读13分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

类型兼容性

两种类型系统:

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

TS采用的是结构化类型系统,也叫做ducktyping(鸭子类型),类型检查关注的是值所具有的形状

也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。

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.因为 TS 是结构化类型系统,只检查Point 和 Point2D 的结构是否相同(相同,都具有x和 y 两个属性,属性类型也相同)。

4.但是,如果在 Nominal Type System 中(比如,C#、Java 等),它们是不同的类,类型无法兼容。


对象之间的类型兼容性

注意:在结构化类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型,这种说法并不准确。

更准确的说法:对于对象类型来说,y的成员至少与x相同,则x兼容y(成员多的可以赋值给少的)。

class Point {x: number; y: number }
class Point3D {x: number; y: number; z: number }
const p: Point = new Point3DC()
//错误演示
const p2:Point3D = new Point()

解释:

1. Point3D的成员至少与Point相同,则Point兼容Point3D。

2. 所以,成员多的 Point3D 可以赋值给成员少的 Point。


接口之间的类型兼容性

接口之间的兼容性,类似于 class。并且,class 和 interface 之间也可以兼容。

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

class Point3D {x: number; y: number; z: number 
let p3: Point2D = new Point3D()

类型兼容性原则总结:

成员多的可以赋值给成员少的,不管它是接口还是类


函数之间的类型兼容性

函数之间兼容性比较复杂,需要考虑: 1.参数个数;2.参数类型;3.返回值类型。

一、参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)。

type F1 = (a: number) => void
type F2 = (a: number, b: number) => void
let f1: F1
let f2: F2 = f1

const arr = ['a', 'b', 'c']
arr. forEach(C) => {})
arr. forEach((item)

解释:

1.参数少的可以赋值给参数多的,所以,f1 可以赋值给f2。

2.数组forEach方法的第一个参数是回调函数,该示例中类型为: (value: string, index: number, array: stringl]) =>void。

3.在JS中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了TS中函数类型之间的兼容性。

4. 并且因为回调函数是有类型的,所以,TS会自动推导出参数item,index, array的类型。

二、参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)。

type F1 = (a: number)=>string
type F2 = (a: number)=>string
let f1: F1
let f2: F2 = f1

解释:函数类型 F2 兼容函数类型 F1,因为 F1 和 F2 的第一个参数类型相同。

又比如:

解释:

1,注意,此处与前面讲到的接口兼容性冲突。

2.技巧:将对象拆开,把每个属性看做一个个参数,则,参数少的(f2)可以赋值给参数多的(f3)。

三、返回值类型,只关注返回值类型本身即可:

type F5 = ()=> string
type F6 = ()=>string
let f5: F5
let f6: F6 = f5

type F7 ={ name: string }
type F8 = =>{ name: string; age: number }
let f7: F7
let f8: F8
f7 = f8

解释:

1. 如果返回值类型是原始类型,此时两个类型要相同,比如,左侧类型F5和F6。

2. 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如,右侧类型F7和F8。


交叉类型

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

如:

解释:使用交叉类型后,新的类型PersonDetail就同时具备了Person和Contact的所有属性类型。

相当于,


交叉类型和接口传承的对比

相同点:都可以实现对象类型的组合。

不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。

interface A {
fn: (value: number) => string
}
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 I number) => string

交叉类型小结

1、交叉类型可以实现将多个类型组合成一个新类型的功能

2、交叉类型还可以实现函数重载的功能,因为接口传承无法做到所以才有了交叉类型


泛型

泛型是可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数、接口、class中。

需求:创建一个id函数,传入什么数据就返回该数据本身(也就是说,参数和返回值类型相同)。

function id(value: number): number { return value }

比如, id(10)调用以上函数就会直接返回10本身。但是,该函数只接收数值类型,无法用于其他类型。

为了能让函数能够接受任意类型,可以将参数类型修改为any。但是,这样就失去了TS的类型保护,类型不安全。

function id(value: any): any { return value }

泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用。

实际上,在C#和Java等编程语言中,泛型都是用来实现可复用组件功能的主要工具之一。

function id<Type>(value: Type): Type { return value }

解释:

1. 语法:在函数名称的后面添加<> (尖括号) ,尖括号中添加类型变量,比如此处的Type。

2. 类型变量 Type,是一种特殊类型的变量,它处理类型而不是值。

3.该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)。

4. 因为Type是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型。

5.类型变量Type,可以是任意合法的变量名称。

const num number
const num = id<number>(10)

const str: string
const str = id<string>('a')

解释:

1. 语法:在函数名称的后面添加<> (尖括号) ,尖括号中指定具体的类型,比如,此处的 number。

2. 当传入类型 number后,这个类型就会被函数声明时指定的类型变量Type捕获到。

3.此时, Type的类型就是number,所以,函数id参数和返回值的类型也都是number

同样,如果传入类型string,函数id参数和返回值的类型就都是string。

这样,通过泛型就做到了让 id 函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全。


使用泛型的时机

当你想用函数/class类/接口要支持多种类型的数据同时又想保证类型安全时就可以使用泛型


化简调用泛型函数

function id<Type>(value: Type): Type { return value }

解释:

1.在调用泛型函数时,可以省略<类型>来简化泛型函数的调用。

2.此时,TS内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量Type的类型。

3.比如,传入实参 10,TS会自动推断出变量 num的类型 number,并作为Type的类型。

推荐:使用这种简化的方式调用泛型函数,使代码更短,更易于阅读。

说明:当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数。


泛型约束

泛型约束:默认情况下,泛型函数的类型变量Type可以代表多个类型,这导致无法访问任何属性。

比如,id('a')调用函数时获取参数的长度:

解释:Type可以代表任意类型,无法保证一定存在length属性,比如number类型就没有length。

此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)。

添加泛型约束收缩类型,主要有以下两种方式: 1指定更加具体的类型2添加约束。

function id<Type>(value: Type[]): Type[]{
console. log(value. length)
return value
}

比如,将类型修改为Type[] (Type类型的数组) ,因为只要是数组就一定存在length属性,因此就可以访问了。

interface ILength { length: number }
function id<Type extends ILength>(value: Type): Type {
console.log(value. length)
return value
}

解释:

1.创建描述约束的接口ILength,该接口要求提供length属性。

2.通过extends关键字使用该接口,为泛型(类型变量)添加约束。

3,该约束表示:传入的类型必须具有length属性

注意:传入的实参(比如,数组)只要有length属性即可,这也符合前面讲到的接口的类型兼容性。


多个泛型变量的情况

泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)。

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

解释:

1.添加了第二个类型变量Key,两个类型变量之间使用(,)逗号分隔。

2.keyof关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。

3.本示例中keyof Type实际上获取的是person对象所有键的联合类型,也就是:'name'l'age'。

4.类型变量Key受Type约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性。

不是对象也有key,key的值是约束前面的包装属性,这一点就体现了万物皆对象,函数执行的时候就会调用传入的第二个参数的方法对第一个参数进行处理,其中字符串类型的key中number的意思是索引字符串中第number个字符。不过一般都是用来访问对象中的键值对


泛型接口

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

interface IdFunc<Type>{
id: (value: Type)=>Тype
ids: ()=>Type[]
}

let obj: IdFunc<number> ={
id(value) { return value },
ids() { return [1, 3, 5] }
}
3

解释:

1.在接口名称的后面添加<类型变量>,那么,这个接口就变成了泛型接口。

2.接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量。

3.使用泛型接口时,需要显式指定具体的类型(比如,此处的IdFunc)。

4,此时,id方法的参数和返回值类型都是 number;ids方法的返回值类型是number[]。

实际上js的数组就是一个泛型接口

当我们在使用数组时,TS会根据数组的不同类型,来自动将类型变量设置为相应的类型。

技巧:可以通过Ctrl+鼠标左键(Mac:option+鼠标左键)来查看具体的类型信息。


泛型类

泛型类:class也可以配合泛型来使用。

比如, React的class组件的基类Component就是泛型类,不同的组件有不同的props和state

interface IState { count: number }
interface IProps { maxLength: number }
class InputCount extends React. Component<IProps, IState> {
state: IState = {
count: 0
}
render() {
return <div>{ this. props. maxLength}</div>
}
 } 

解释:React.Component泛型类两个类型变量,分别指定props和state类型。

class GenericNumber<NumType> {
defaultValue: NumType
add: (x: NumType, y: NumType) => NumType
}

解释:

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

2.此处的 add方法,采用的是箭头函数形式的类型书写方式。

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

类似于泛型接口,在创建 class 实例时,在类名后面通过<类型> 来指定明确的类型。


泛型工具类型

泛型工具类型:TS 内置了一些常用的工具类型,来简化TS 中的一些常见操作。

说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。

这些工具类型有很多,主要学习以下几个:

1. Partial

2. Readonly

3. Pick<Type, Keys>

4. Record<Keys, Type>

interface Props {
id: string
children: number[]
}
type PartialProps = Partial<Props>

解释:构造出来的新类型PartialProps结构和Props相同,但所有属性都变为可选的。

如:

Let p1: Props = {
id: '',
children: [1]
}
//PartialProps是通过Partial构建出来的,它的属性是可选的
Let p2: PartialProps ={
id:''
}

interface Props {
id: string
children: number[]
}	
type ReadonlyProps = Readonly<Props>

泛型工具类型-Readonly用来构造一个类型,将Type的所有属性都设置为readonly(只读)。

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

当我们想重新给id 属性赋值时,就会报错:无法分配到"id",因为它是只读属性。

interface Props {
id: string 
title: string 
children: number[]
}
type PickProps = Pick<Props, 'id'| 'title'>

泛型工具类型-Pick<Type,Keys>从Type中选择一组属性来构造新类型。

解释:

1.Pick工具类型有两个类型变量:1表示选择谁的属性2表示选择哪几个属性。

2.其中第二个类型变量,如果只选择一个则只传入该属性名即可。

3.第二个类型变量传入的属性只能是第一个类型变量中存在的属性。

4.构造出来的新类型 PickProps,只有id 和title两个属性类型。

type RecordObj = Record<'a'|'b'|'c', string[]> 
  let obj: RecordObj = {
a: ['1'],
b: ['2'], 
c: ['3']
}

泛型工具类型-Record<Keys,Type> 构造一个对象类型,属性键为Keys,属性类型为 Type。

解释:

1.Record工具类型有两个类型变量:1表示对象有哪些属性2表示对象属性的类型。

2.构建的新对象类型Recordobj 表示:这个对象有三个属性分别为a/b/c,属性值的类型都是string[]。


索引签名类型

绝大多数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型。

使用场景:当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时,就用到索引签名类型了。

interface AnyObject{
[key: string]: number
}

let obj: AnyObject ={
a: 1,
b: 2,
}

解释:

1.使用[key:string]来约束该接口中允许出现的属性名称。表示只要是string类型的属性名称,都可以出现在对象中。

2.这样,对象obj中就可以出现任意多个属性(比如,a、b等)。

3.key只是一个占位符,可以换成任意合法的变量名称。

4.隐藏的前置知识:JS中对象({})的键是string类型的。

索引签名类型的特点:只要符合条件的属性实例化对象的时候可以任意添加,这一点用泛型是做不到的,因为泛型需要提前知道属性名,而签名索引则可以匹配任何符合要求的属性

一些可知道可不知道的知识:

在 JS 中数组是一类特殊的对象,特殊在数组的键(索引)是数值类型。

并且,数组也可以出现任意多个元素。所以,在数组对应的泛型接口中,也用到了索引签名类型。

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

解释:

1.MyArray 接口模拟原生的数组接口,并使用[n:number]来作为索引签名类型。

2,该索引签名类型表示:只要是number类型的键(索引)都可以出现在数组中,或者说数组中可以有任意多个元素。

3.同时也符合数组索引是number类型这一前提。