初识TypeScript
TypeScript学习!
TypeScript 是什么
- TypeScript(简称:Ts)是 JavaScript 的超集(Js 有的 Ts 都有)
- TypeScrip = Type + JavaScript(在 Js 基础之上,为 js 添加了类型支持)
- TypeScript 是微软开发的开源编程语言,可以在任何运行 JavaScript 的地方运行
// typescript 代码:有明确的类型,即:number(数值类型)
let age1:number = 18
// javascript 代码: 无明确的类型
let age2 = 18
1.1 typescript 为什么要为 js 添加类型支持?
背景:js 的类型系统存在”先天缺陷”,js 代码中绝大部分错误都是类型错误(UncaughtTypeError)。
问题:增加了找 Bug、改 Bug 的时间,严重影响开发效率。
从编程语言的动静来区分,TypeScript 属于静态类型的编程语言,JS 属于动态类型的编程语言。
静态类型:编译器做类型检查;
动态类型:执行器做类型检查
代码编译和代码执行的顺序:1.编译,2.运行。
对于 Js 来说:需要等到代码真正去执行的时候才能发现错误(晚)
对于 Ts 来说:在代码编译的时候(代码执行前)就可以发现错误(早)
并且,配合 VScode 等开发工具,Ts 可以提前到在编写代码的同时就发现代码中的错误,减少找 bug,改 bug 时间
1.2TypeScript 相比 Javascript 的优势
- 更早(写代码的同时)发现错误,减少找 bug、改 bug 时间,提升开发效率。
- 程序中任何位置的代码都有代码提示,随时随地的安全感,增强了开发体验
- 枪法的类型系统提升了代码的可维护性,使得代码更加容易。
- 支持最新的 ECMAScript 语法,有限体验最新的语法,让你走在前端及参数的最前沿
- Ts 类型推断机制,不需要再代码中的每个地方都显示标注类型,让你再享受优势的同时,尽量降低了成本。
除此之外,Vue3 源码使用 Ts 重写、Angular 默认支持 Ts、React 与 Ts 完美配合,TypeScript 已成为大中型前端项目的首先编程语言
TypeScript 初体验
- 安装编译 Ts 的工具包
为什么要安装编译 Ts 的工具包?
回答:Node.js/浏览器,只认识 Js 代码,不认识 Ts 代码。需要先将 Ts 代码转化为 Js 代码,任何才能运行
安装命令:npm i -g typescript
typescript 包:用来编译 Ts 代码的包,提供了 tsc 命令,实现 Ts -> js 的转化
验证是否安装成功: tsc -v (查看 typescript 的版本)
编译并运行 Ts 代码
- 创建 hello.ts 文件(注意:TS 文件的后缀名为.ts)
- 将 Ts 编译为 js:在终端中输入命令,tsc hello.ts (此时,在统计目录中会出现一个同名的 Js 文件)。
- 执行 js 代码:在终端中输入命令:node hello.js
说明:所有合法的 JS 代码都是 TS 代码,有 JS 基础只需要学习 TS 的类型即可。
注意:由 TS 编译生成 JS 文件,代码中就没有类型信息了。
<!-- Ts 文件 -->
let age:Number = 18
console.log(age)
简化运行 Ts 的步骤
- 问题描述:每次修改代码后,都要重复执行两个命令,才能运行 Ts 代码,太繁琐。
- 简化方式:使用 ts-node 包,直接在 node.js 中执行 Ts 代码
- 安装命令:npm i -g ts-node (ts-node 包提供了 ts-node 命令)。
- 使用方式:ts-node hello.ts
- 解释:ts-node 命令在内部偷偷的将 Ts -> js,然后再运行 js 代码
TypeScript 常用类型
概述
TypeScript 是 js 的超集,Ts 提供了 Js 的所有功能,并且额外的增加了:类型系统
- 所有的 Js 代码都是 Ts 代码
- js 有类型(比如,number/string 等等),但 js 不会检查变量的类型是否发生变化。儿 Ts 会检查。TypeScript 类型系统的主要优势:可以显示标记处代码中的意外行为,从而降低了发生错误的可能性
- 类型注解
- 常用基础类型
let age: number = 18
// 错误行为
// age = '20'
age.toFixed()
类型注解
实例代码:
let age:number = 18
说明:代码中的:number 就是类型注解。
作用:为变量添加类型约束。比如上述代码中,约定变量 age 的类型为 number(数值类型)。
解释:约定了什么类型,就只能给变量赋值改类型的值。
let age: number = 18
age = 29
// 错误
// age = '99'
常用基础类型
可以将 Ts 中的常用基础类型细分为两类:
- js 已有类型
- 原始类型有:number/string/boolean/null/undefined/symbol
- 对象类型:object(数组、对象、函数等对象)
- Ts 新增类型
- 联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any 等
原始类型
- 原始类型:number/string/boolean/null/undefined/symbol
- 特点:简单。这些类型,完全按照 js 中类型的名称来书写
let age: number = 18
let mu: string = 'shdh'
let isl: boolean = false
let a: null = null
let b: undefined = undefined
let s: symbol = Symbol()
数组类型
- 对象类型:object(数组、对象、函数等对象)。
特点:对象类型,再 Ts 中更加细化,每个具体的对象都有自己的类型语法。
- 数组类型的两种写法:(推荐 number[]写法)
let numbers: number[] = [1,3,4]
let strings: Array<number> = ['a','b','c']
联合类型
需求:数组中既要有 number 类型,又要有 string 类型这个数组类型怎么写?
答案:
let arr: (number | string)[] = [1,'a',3,'c']
扩充
// 添加小括号,表示:首先是数组,然后,这个数组中能够出现number或string类型的元素
let arr: (number | string)[] = [1,3,4,6,"a","b"]
// 不添加小括号,表示:arr1 既可以是number类型,又可以是string[] 这是错误案例
let arr1: number | string[] = ['a','b']
let arr1: number | string[] = 123
解释:|(竖线)在 Ts 中叫做联合类型(由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种)。
注意:这是 Ts 中联合类型的语法,只有一根竖线,不要与 js 中的逻辑或(||)混乱了。
类型别名
类型别名(自定义类型):为任意类型起别名。
使用场景:当同一类型(复杂)被多次使用时,可以通过类型别名,简化改类型的使用。
type CustomArray = (number | string)[]
let arr1: CustomArray = [1,'a',3,'b']
let arr2: CustomArray = [4,'x',6,'y']
解释:
- 使用 type 关键字来创建类型别名
- 类型别名(比如,此处的 CustomArray),可以是任意合法的变量名称。
- 创建类型别名后,直接使用该类型别名作为变量的类型注解即可。
函数类型
函数的类型实际上指的是:函数参数和返回值的类型。
为函数指定类型的两种方式:
- 单独指定参数、返回值的类型
- 单独指定参数、返回值的类型
// 函数
function add(num1:number,num2:number):number{
return num1 + num2
}
// 函数表达式
const add = (num1:number,num2:number):number{
return num1 + num2
}
- 同时指定参数、返回值的类型
- 函数的类型实际上指的是:函数参数和返回值的类型
- 为函数指定类型的两种方式:
- 单独指定参数、返回值的类型
- 同时指定参数、返回值的类型
- 同时指定参数、返回值的类型:
const add:(num1:number,num2:number) => number =(num1,num2)=>{
return num1 + num2
}
解释:当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型。
注意: 这种形式只适用于函数表达式。
如果函数没有返回值,那么,函数返回值类型为:void
function greet(name:string): void{
console.log('hello',name)
}
可选参数:
使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,就用到可选参数了。比如,数组的 slice 方法,可以 slice()也可以 slice(1) 还可以 slice(1,3)
function mySlice(start?: number,end?: number):void {
console.log('起始索引:'start,'结束索引:',end)
}
可选参数:在可传不可传的参数名称后面添加?(问号)。
注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。
对象类型
js的对象室友属性和方法构成的,而TS中对象的类型就是在描述对象的结构(有什么类型的属性和方法)
对象类型给的写法:
let person:{
name:string;
age:number;
sayHi():void
} = {
name: 'jack',
age:19,
sayHi(){}
}
解释:
- 直接使用{}来描述对象结构。属性采用属性名:类型的形式;方法采用方法名():返回值类型的形式
- 如果方法有参数,就在方法名后面的小括号中指定参数类型比如(greet(nam:string):void)
- 在一行代码中指定对象的多个属性类型时,使用;(分号)来分隔。
- 如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉;(分号)。
- 方法的类型也可以使用箭头函数形式(比如:{sayHi:()=>void})
对象可选属性
对象的属性或方法,也可以以是可选的,此时就用到了可选属性了
比如,我们在使用axios({….})时,如果发送GET请求,method属性就可以省略。
function myAxios(config:{url:string;method?:string}){
console.log(config )
}
可选属性的语法与函数可选参数的语法一致,都使用?(问号)来表示。
接口
当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的。
解释:
- 使用interface关键字来声明接口。
- 接口名称(比如,此处的IPerson),可以是任意合法的变量名称
- 声明接口后,直接使用接口名称作为变量的类型
- 因为每一行只有一个属性类型,因此,属性类型后没有;(分号)
interface IPerson{
name:string
age:number
sayHi():void
}
let person: IPerson ={
name:'jack',
age:19,
sayHi(){}
}
接口和类型别名的对比
interface(接口)和type(类型别名的对比)
相同点:都可以给对象执行类型
不同点:
- 接口,只能为对象指定类型。
- 类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名
- 接口
interface IPerson{ name:string age:number sayHi(): void }
- 类型别名
type IPerson = { name:string age:18 sayHi():void }
type NUmStr = number | string
接口继承
如果两个接口之间有相同的属性或方法, 可以将公共的属性或方法抽离出来,通过继承来实现复用
比如:这两个接口都有x,y两个属性,重复写两次,太繁琐了
interface Point2D { x:number;y:number }
interface Point3D { x:number;y:number;z:number }
更好的方法:
interface Point2D { x:number;y:number }
interface Point3D extends Point2D { z:number }
解释:
- 使用extends(继承)关键字实现接口Point3D继承Point2D
- 继承后,Point3D就有Point2D的所有属性和方法(此时,Point3D同时有x,y,z三个属性)
元组
场景:在地图中,使用经纬度坐标来标记位置信息
可以使用数组记录坐标,那么,该数组中只有两个元素,并且这两个元素都是数值类型。
let position: number[] = [49.5427,116.2317]
使用number[]的缺点:不眼睛,因为该类型的数组中可以出现任意多个数字
刚好的方式:元素(Tuple)。
元组类型是另一种数据的数组,它确切地知道多少个元素,一级特定索引对应的类型。
let position : [number,number] = [39.5427,116.2317]
解释:
- 元组类型可以确切地标记处有多少个元素,一级每个元素的类型
- 该实例中,元素有两个元素,么个元素的类型都是number
类型推论
在TS中,某些没有明确指出类型的地方,ts的类型推论机制会帮助提供类型
换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写
发生类型推论的2种场景:
- 声明变量并初始化时
- 决定函数返回值时
let age:number // Ts自动推断处变量age 为number类型
let age = 18 // 鼠标移入变量名称age
注意:这两种情况下,类型注解可以省略不写!
推荐:能省略类型注解的地方就省略(充分利用TS类型推论的能力,提升开发效率)
技巧:如果不知道类型,可以通过鼠标放在变量名称上,利用VScode的提示来查看类型
类型断言
有时候你会比TS更明确的类型,此时,可是使用类型断言来指定更具体的类型
比如:
<a href="http://www.itcast.cn" id="link">传智教育</a>
const aLink: HTMLElement
const aLink = document.getElementById('link')
注意:getElementById方法返回值的类型是HTMLElement,该来行值包含所有标签公共的属性或方法,不包含a标签特有的href等属性
因此,这个类型太宽泛(不具体),无法操作href等a标签特有的属性或方法
解决方法:这种情况下就需要使用类型断言指定更加具体的类型
使用类型断言:
const aLink:HTMLAnchorElement
const aLink = document.getElementById('link') as HTMLAnchorElement
解释:
- 使用as关键字实现类型断言
- 关键字as后面的类型是一个更加具体,这样就可以访问a标签特有的属性或方法了
- 通过类型断言,aLink的类型变得更加具体,这样就可以访问a标签特有的属性或方法了
另一种语法,使用<>语法,这种语法形式不常用知道即可:
技巧:在浏览器控制台,通过console.dir()打印DOM元素,在属性列表的最后面,即可看到该元素的类型const aLink = <HTMLAnchorElement>document.getElementById('link')
字面量类型
思考一下代码,两个变量的类型分别是什么?
let str1 = 'Hello Ts'
const str2 = 'Hello Ts'
通过Ts类型推论机制,可以得到答案:
- 变量str1的类型为:string
- 变量str2的类型为: ‘Hello TS’
解释: - str1是一个变量(let),它的值可以是任意字符串,所有类型为:string
- str2是一个常量(const),它的值不能变化只能是’Hello TS’,所以,它的类型为:’Hello TS’
注意:此处的’Hello TS’,就是一个字面量类型。也就是说某个特定的字符串的字符串也可以作为TS中的类型。除字符串外,任意的JS字面量(比如,对象、数字等)都可以作为类型使用
使用模式:字面量类型配置联合类型一起使用
使用场景:用来表示一组明确的可选值列表
比如,在贪吃蛇游戏中,游戏的方向可选值只能是上、下、左、右中任意一个
function changDirection(direction:'up' | 'down' | 'left' | 'right'){
console.log(direction)
}
解释:参数direction的值只能是up / down / left / right中的任意一个
优势:相比于string类型,使用字面量类型更加精确,严谨
枚举
枚举的功能类似字面量类型+联合类型的功能,也可以表示一组明确的可选值
枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个
enum Direction {Up,Down,Left,Right}
function changeDirection(direction:Direction){
console.log(direction)
}
解释:
- 使用enum关键字定义枚举
- 约定枚举名称、枚举中的值以大写字母开头
- 枚举中的多个值之间通过,(逗号)分隔。
- 定义好枚举后,直接使用枚举名称作为类型注解
注意:形参direction的类型为枚举Direction,那么,实参的值就应该是枚举Direction成员的任意一个
访问枚举成员:
enum Direction {Up,Down,Left,Right}
function changeDirection(direction:Direction){
console.log(direction)
}
changeDirection(Direction.Up)
解释:类似js中的对象直接通过点(.)语法访问枚举的成员
枚举成员的值一级–数字枚举
问题:我们把枚举成员作为了函数的实参,它的值是什么呢?
(enum member) Direction.up = 0 //这是鼠标移入的
changeDirection(Direction.up)
解释:通过鼠标移入Direction.Up,可以看到枚举成员Up的值为0
注意:枚举成员是有值的,默认为:从0开始自增的数值
我们把,枚举成员的值为数字的枚举,称为:数字枚举
当然,也可以给枚举中成员初始化值
// Down -> 11, Left -> 12, Right -> 13
enum Direction {Up= 10,Down,Left,Right}
enum Direction {Up =2, Down = 4, Left = 8, Right = 16}
字符换枚举:枚举成员的值是字符串
enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值
枚举的特点及原理
枚举是Ts为数不多的非javascript类型级扩展(不仅仅是类型)的特性之一
因为:其他类型仅仅被当作类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)
也就是说,其他的类型会在编译为js代码时自动移除。但是,枚举类型会被编译为js代码
Ts代码
enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
被编译为Js代码
var Direction;
(function (Direction){
Direction['Up'] = 'UP'
Direction['Down'] = 'Down'
Direction['Left'] = 'Left'
Direction['Right'] = 'UP'
})(Direction || (Direction = {}))
说明:枚举与前面讲到的字面量类型+联合类型组合的功能类似,都用来表示一组明确的可选值列表
一般情况下,推荐使用字面量类型+联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效
any类型
原则:不推荐使用any! 这会让TypeScript变为“AnyScript”(事情Ts类型保护的优势)
因为当值类型为any时,可以对改值进行任意操作,并且不会有代码提示。
let obj: any = {x:0}
obj.bar = 1000
obj()
const n: number = obj
解释:以上操作都不会有任何类型错误提示,即使可能存在错误!
尽可能的避免使用any类型,除非临时使用any来“避免”书写很长、很复杂的类型
其他隐式具有any类型的情况:
- 声明变量不提供类型也不提供默认值
- 函数参数不加类型
注意:因为不推荐使用any,所以,这两种情况下都应该提供类型!
typeof运算符
大家都知道,js提供了typeof操作符,用来在js中获取数据的类型
console.log(typeof 'hello') // 打印string
实际上,Ts也提供了typeof操作符:可以在类型上下文中引用变量或书写的类型(类型查询)
使用场景:根据已有变量的值,获取该值的类型,来简化类型书写。
let p = {x:1,y:2}
function formatPoint(point:{x:number,y:number}){}
formatPoint(p)
function formatPoint(point:typeof p){}
解释:
- 使用typeof操作符来获取变量p的类型,结果与第一种(对象字面量形式的类型)相同。
- typeof出现在类型注解的位置(参数名称的冒号后面)所处的环境就在类型上下文(区别于js代码)
- 注意:typeof只能用来查询变量或属性的类型,无法查询其他形式的类型(比如,函数调用的类型)
TypeScript 高级类型
- 概述
- class类
- 类型兼容性
- 交叉类型
- 泛型和keyof
- 索引签名类型 和 索引查询类型
- 映射类型
class类
TypeScript全面支持ES2015中引入的class关键字,并未其添加了类型注解和其他语法(比如,可见性修饰符等)
class基本使用,如下:
解释:class Person{} const p :Person const p = new Person()
- 根据Ts中的类型推论,可以知道Person类的实例对象p的类型时Person
- Ts中的class,不仅提供了class的语法功能,也作为一种类型存在。
实例属性初始化:
class Person{
age: number,
gender = '男'
}
解释:
- 声明成员age,类型为number(没有初始值)
- 声明成员gender,并设置初始值,此时,可省略类型注解(TS类型推论为string类型)
class的构造函数
- 构造函数:
解释:class Person{ age:number gender:string } constructor(age:number,gender:number){ this.age = age this.gender = gender }
- 成员初始化(比如,age:number)后,才可以通过this.age来访问实例成员
- 需要为构造函数指定类型注解,否则会被隐式推断为any;构造函数不需要返回值类型。
class实例方法
- 实例方法:
```
class Point {
x = 10
y = 20
scale(n:number):void{
this.x *=n
this.y *=n
}
}
const p = new Point()
p.scale(10)
console.log(p.x,p.y) // x= 100 , y = 200
解释:方法的类型注解(参数和返回值)与函数用法相同
##### class继承
- 类继承的两种方式
1. extends(继承父类)
2. implements(实现接口)
说明:Js只有extends,而implements是TS提供的
##### class继承 (extends)
class Animal{
move(){
console.log(‘aabbc’)
}
}
class Dog extends Animal{
back() {
console.log(‘汪’)
}
}
const dog = new Dog()
// 可以使用父类身上的move方法
console.log(dog.move()) // aabbc
解释:
1. 通过extends关键字实现继承
2. 子类Dog类继承父类Animal,则Dog的实例对象dog就同时具有了父类Animal和子类Dog的所有属性和方法
##### class继承 (implements)
interface Singable{
sing():void // 方法
name: string // 属性
}
class Person implements Singable {
name = ‘111’,
sing(){
console.log(‘11111111’)
}
}
解释:
1. 通过implements关键字让class实现接口。
2. Person类实现接口Singable意味着,Person类中必须提供Singable接口中指定的所有方法和属性
##### class类可见性修饰符
类成员可见性:可以使用TS来控制class的方法或属性对于class外的代码是否课件
可见性的修饰符包括
1. public(公开的)
2. protected(受保护的)
3. private(私有的)
##### class类可见性修饰符 (public)
- public:表示公开的,共有成员可以被任何地方访问,默认可见性
// 父类
class Animal {
public move(){
console.log(‘Moving along!’)
}
}
// 子类
class Dog extends Animal{
back(){
console.log(‘111’)
}
}
const d = new Dog()
d.move() //这里可以访问到
解释:
1. 在类属性或方法前面添加public关键字,来修饰该属性或方法是共有的
2. 因为public是默认可见性,所以,可以直接省略不写
##### class类可见性修饰符 (protected)
- protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见。
class Animal{
// 这个方法是受保护的
protected move(){console.log(‘Moving along!’)}
}
class Dog extends Animal {
back(){
console.log(‘汪!’)
this.move()
}
}
const d = new Dog()
d.move() //访问不到
解释:
1. 在类属性或方法前面添加protected关键字,来修饰该属性或方法是收保护的
2. 在子类的方法内部可以通过this来访问父类中收保护的成员,但是,对实例不可见!
##### class类可见性修饰符 (private)
- private:表示私有的,旨在当前类中课件,对实例对象以及子类也是不可见的
class Animal{
// 这个方法是受保护的
protected move(){console.log(‘Moving along!’)}
walk(){
// 只能在当前类中进行调用
this.move()
}
}
解释:
1. 在类属性或方法前面添加private关键字,来修饰该属性或方法是私有的
2. 私有的属性或方法旨在当前类中课件,对子类和实例对象也都是不可见的!
##### readonly 只读修饰符
除了可见性修饰符之外,还有一个常见修饰符就是:readonly(只读修饰符)
readonly:表示只读,用来防止在构造函数之外对属性进行赋值
class Person{
// 只读属性
// 注意:只要是readonly 来修饰的属性,必须手动提供明确的类型
readonly age: number = 18
constructor (age:number){
this.age = age
}
}
// 接口
interface IPerson {
// 设置了只读属性
readonly name : string
}
let obj: IPerson = {
name: ‘jack’
}
obj.name = ‘rose’
// 对象指定类型
let obj: {readonly name : string}={
name: ‘jack’
}
obj.name = ‘rose’
解释:
1. 使用**readonly**关键字修饰符该属性是只读的,注意**只能修饰属性不能修饰方法**
2. 注意:属性age后面的类型注解(比如,此处的number)如果不加,则age的类型为18(字面量类型)
3. **接口或者{}表示对象类型**,也可以使用readonly.
#### 类型兼容性
两种类型系统:
1. StructuralType System (结构化类型系统)
2. Nominal Type System (标明类型系统)
TS采用的是结构化类型系统,也叫做duck typing(鸭子类型),类型检查关注的是值所具有的形状
也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型
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}
class p: Point = new Point3D()
// 错误演示
const p2:Point3D = new Point()
解释:
1. Point3D的成员至少与Point相同,则Point兼容Point3D
2. 所以,成员多的Point3D可以赋值给成员少的Point
##### 类型兼容性
除了class之外,Ts中的其他类型也存在相互兼容的情况,包括
1. 接口兼容性
2. 函数兼容性
- 接口的兼容性,类型于class。并且,class和interface之间也可以兼容
interface Point {x:number,y:number }
interface Point2D {x:number,y:number,z: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
// 错误演示
f1 = f2
const arr = [‘a’,’b’,’c’]
arr.forEach(()=>{})
arr.forEach((item)=>{})
解释:
1. 参数少的可以赋值给参数多的,所以,f1可以赋值给f2
2. 数组forEach方法的第一个参数是回调函数,该实例中为:(value:string,index:number,array:string[])=> void
3. 在js中省略用不到的函数参数实际上是很常见的,这样的使用方式,吹九年了TS中函数类型之间的兼容性
4. 并且因为回调函数是由类型的,所以TS会自动推导处参数item\index\array的类型
##### 函数之间的类型兼容性 (函数参数)
- 函数之间兼容性比较复杂,需要考虑:
1. 参数个数
2. 参数类型
3. 返回值类型
- 参数类型,相同位置的参数类型要相同(原始类型)兼容(对象类型)
type F1 = (a:number) => string
type F2 = (a:number) => string
let f1:F1
let f2:F2 = f1
解释:函数类型F2兼容函数类型F1,因为F1和F2的第一俄国参数类型相同
// 原始类型
type F1 = (a:number) => void
type F2 = (a:number) => void
let f1:F1
let f2:F2
f1 = f2
// 对象类型
interface Point2D{
x:number
y:number
}
interface Point3D{
x:number
y:number
z:number
}
##### 函数之间的类型兼容性(返回值)
- 函数之间兼容性比较复杂,需要考虑
1. 参数个数
2. 参数类型
3. 返回值类型
- 返回值类型,只关注返回值类型本身即可
type F5 =() =string
type F6 =() = string
let f5:F5
let f6:F6
f5 = f6
type F7 = () => {name:string}
type F8 = () => {name:string,age:number}
let f7:F7
let f8:F8
f7 = f8
f8 = f7
解释:
1. 如果返回值类型是原始类型,此时两个类型要相同,比如上面的F5和F6
2. 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如,上面的F7和F8
#### 交叉类型
交叉类型(&):功能类似接口继承(extends)用于组合多个类型为一个类型(常用于对象类型)
比如:
interface Person{name:string}
interface Contact {phone:string}
type PersonDetail = Person & Contact
let obj:PersonDetail ={
name:’jack’,
phone:’1333…..’
}
解释:使用交叉类型后,心的类型PersonDetail就同时具备了Person和Contact的所有属性类型
相当于,
type Person Detail = {name:string;phone:string}
##### 交叉类型和接口之间的对比说明
- 交叉类型(&)和接口结成(extends)的对比:
- 相同点:都可以实现对象类型的组合
- 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同
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 | number) => string
#### 泛型和keyof
##### 泛型的基本使用
泛型时可以在保证类型安全前提下,让函数等多种类型一起工作,从而实现复用,常用于:函数、接口、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
解释:
1. 语法:在函数名称的后面添加<>(尖括号),尖括号中添加类型变量,比如此处的Type
2. 类型变量Type,是一种特殊类型的变量,它处理类型而不是值
3. 该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)。
4. 因为Type时类型。因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型
5. 类型变量Type,可以时任意合法的变量名称
- 调用泛型函数:
function id
const num = id
const str = id
解释:
1. 语法:在函数名称的后面添加<>(尖括号),尖括号中指定具体的类型,比如,此处的number
2. 当传入类型number后,这个类型就会被函数声明时指定的类型变量Type捕获到
3. 此时,Type的类型就是number,所以函数,id参数和返回值的参数也都是number
同样,如果传入类型string,函数id参数和返回值的类型就都是string
这样,通过泛型就做到了让id函数于多种不同的类型一起工作,实现了复用的同时保证了类型安全
##### 简化泛型函数调用
function id
let num = id
let num = id(10) === let num:number
解释:
1. 在调用泛型函数时,**可以省略<类型>来简化泛型函数的调用**
2. 此时,TS内部会次啊用一种叫做类型参数推断的机制,来根据传入的实参自动推断处类型变量Type的类型
3. 比如,传入实参10,Ts会自动推断出变量num的类型number,并作为Type的类型
推荐:使用这种简化的方式调用泛型函数,使代码更短,更易于阅读。
说明:当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数。
##### 泛型结束
- 泛型结束:默认情况下,泛型函数的类型变量Type可以代表多个类型,这导致无法访问任何属性
比如,id('a')调用函数时获取参数的长度:
function id
console.log(value.length)
return value
}
解释:
Type可以代表任意类型,无法保证一定存在length属性,比如number类型就没有length
此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)
- 添加泛型约束收缩类型,主要由以下两种方式:
1. 指定更加具体的类型
2. 添加约束
- 指定更加具体的类型
function id
console.log(value.length)
return value
}
比如:将类型修改为Type[] (Type类型的数组),因为只要时数组就一定存在length属性,因此就可以访问了。
##### extends添加约束
添加泛型约束收缩类型,主要由以下两种方式:
1. 指定更加具体的类型
2. 添加约束
- 添加约束
解释:
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. 本示例中keyofType实际上获取的时person对象所有键的联合类型,也就是: 'name'|'age'
4. 类型变量Key受Type约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性。
##### 泛型接口
- 泛型接口:接口也可以配合泛型来使用,以增加其灵活性,增强其复用性
interface IdFunc
id:(value:Type) => Type
ids:() => Type[]
}
let obj: IdFunc
id(value) { return value },
ids() { return [1,3,5]}
}
解释:
1. 在接口名称的后面添加<类型变量>,那么,这个接口就变成了泛型接口
2. 接口的类型变量,对接口中所有其他成员课件,也就是接口中所有成员都哭呀使用类型变量
3. 使用泛型接口时,需要显示指定具体的类型(比如:此处的IdFunc<number>)
4. 此时,id方法的参数和返回值类型都是number;ids方法的返回值类型时number[]
##### 数组时泛型接口
实际上js中的数组在TS中就是一个泛型接口
![](https://cdn.jsdelivr.net/gh/veam23/tu-img/img/20230521163538.png)
![](https://cdn.jsdelivr.net/gh/veam23/tu-img/img/20230521163552.png)
解释:当我们在使用数组时,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
}
}
![](https://cdn.jsdelivr.net/gh/veam23/tu-img/img/20230521164338.png)
解释:React.Component泛型类两个类型变量,分别指定props和state类型
##### 泛型工具类型 (Partial)
- 泛型工具类型:TS内置了一些常用的工具类型,来简化TS中的一些常见操作
- 说明:他们都是基于泛型实现的(泛型适用于多种类型,更加通常)并且内置的,可以直接在代码中使用
- 主要学习以下几个
1. Partial<Type>
2. Readonly<Type>
3. Pick<Type,Keys>
4. Record<Keys,Type>
###### 泛型工具类型
- Partial<Type> 用来构造(创建)一个类型,将Type的所有属性设置为可选
interface Props{
id:string
children:number[]
}
type PartialProps = Partial
解释:构造出来的新类型PartialProps结构和Props相同,但所有属性都变为可选的
- Readonly<Type> 用来构造一个类型,将Type的所有属性都设置为readonly(只读)
interface Props {
id: string
children: number[]
}
type ReadonlyProps = Readonly
解释:构造出来的新类型ReadonlyProps结构和Props相同,但所有属性都变为只读的
let props: ReadonlyProps = {id:’1’,children:[]}
props.id = ‘2’
当我们想重新给id属性赋值时,就会报错:无法分配到“id”,因为它是只读属性
- Pick<Type,Keys> 从Type中选择一组属性来构造心类型
interface Props{
id:string
title:string
children:number[]
}
type PickProps = Pick<Props, ‘id’ | ‘title’>
解释:
1. Pick工具类型有两个类型变量
1. 表示选择谁的属性
2. 表示选择哪几个属性
2. 其中第二个类型变量,如果只选择一个则之传入该属性名即可
3. **第二个参数变量传入的属性只能是第一个类型变量中存在的属性**
4. 构造出来的新类型PickProps,只有id和title两个属性类型
- Record<Keys,Type>构造一个对象类型,属性键为Keys,属性类型为Type
type RecordObj = Record<’a’ | ‘b’ | ‘c’,string[]>
let obj:RecordObj = {
a:[‘1’],
b:[‘2’],
c:[‘3’]
}
解释:
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
[n:number]: T
}
let arr:MyArray
解释:
1. MyArray接口模拟原生的数组接口,并使用[n:number]来作为索引签名类型
2. 该索引签名类型表示:只要是number类型的键(索引)都可以出现在数组中或者说数组中可以有任意多个元素
3. 同时也复合数组索引是number类型这一前提
#### 映射类型
- 映射类型:基于旧类型创建新类型(对象类型),减少重复、提升开发效率
- 比如:类型ProKeys 有x/y/z 另一个类型Type1中也有x/y/z,并且Type1中 x/y/z的类型相同:
type PropKeys = ‘x’ | ‘y’ | ‘z’
type Type1 = {x:number;y:number;z:number}
这样书写没错,但x/y/z重复书写了两次。像这种情况,就可以使用映射类型来进行简化。
type PropKeys = ‘x’ | ‘y’ | ‘z’
type Type2 = {[Key in PropKeys] : number}
解释:
1. 映射类型是基于索引签名类型的,所以,该语法类似于索引前面类型,也使用了[]
2. Key in PropKeys 表示Key可以是PropKeys联合类型中的任意一个,类似于for in(let in obj)
3. 使用映射类型创建的新对象类型Type2和类型Type1结构完全相同
4. 注意:映射类型只能在类型别名中使用,不能再接口中使用。
##### keyof
- 映射类型除了根据联合类型创建新类型外,还可以根据对象类型来创建:
type Props = {a:number;b:string; c:boolean}
type Type3 = {[Key in keyof Props]: number}
解释:
1. 首先,先执行keyof Props 虎丘到对象类型Props中所有键的联合类型即: 'a' | 'b' | 'c'
2. 然后,Key in ...就表示Key可以是Props中所有键名称中的任意一个
![](https://cdn.jsdelivr.net/gh/veam23/tu-img/img/20230521180015.png)
##### 分析泛型工具类型Partial的实现
实际上,前面讲到的泛型工具类型(比如,Partial<Type>)都是基于映射类型实现的
比如,Partial<Type>的实现:
![](https://cdn.jsdelivr.net/gh/veam23/tu-img/img/20230521182250.png)
解释:
1. keyof T即keyof Props表示获取Props的所有键,也就是: 'a' | 'b' | 'c'
2. 再[]后面添加?(问号)表示将这些属性变为可选的,一次来实现Partial的功能
3. 冒号后面的T[p]表示获取T中每个键对用的类型,比如,如果是 'a'则类型是number; 如果是'b'则类型是string
4. 最终,新类型PartialProps和旧类型Props结构完全相同,只是让所有类型都变为可选
##### 索引查询类型 (基本使用)
刚刚用到的T[p]语法,再TS中叫做索引查询(访问)类型
- 作用:用来查询属性的类型
type Props = {a:number;n:number;c:number}
![](https://cdn.jsdelivr.net/gh/veam23/tu-img/img/20230521183009.png)
解释:Props['a']表示查询类型Props中属性'a'对应的类型number。所以,TypeA类型为number
注意:**[]中的属性必须存在于被查询类型中**,否则就会报错
##### 索引查询类型(2同时查询多个)
索引查询类型的其他使用方式:同时查询多个索引的类型
type Props = {a:number;b:number;c:boolean}
type TypeA = Props[‘a’|’b’] // string | number
解释:使用字符串字面量的联合类型,获取属性a和b对应的类型,结构为string | number
type TypeA = Props[keyof Props] //string | number | boolean
```
解释:使用keyof操作获取Props中所有键对应的类型木结构为:string | number | boolean
TypeScript 类型声明文件
TS中两种文件类型
Ts中又两种文件类型
- .ts文件
- .d.ts文件
- .ts文件:
- 即包含类型信息又可执行代码
- 可以被编译为.js文件,然后,执行代码
- 用途:编写程序代码的地方
- .d.ts文件:
- 值包含类型信息的类型声明文件
- 不会生成.js文件,进用于提供类型信息
- 用途:为js提供类型信息
总结:
.ts是implementation(代码实现文件);
.d.ts是declaration(类型声明文件)
如果腰围js库提供类型信息,咬使用.d.ts文件。
类型声明文件
再使用Ts开发项目时,类型声明文件的使用包括以下两种方式:
- 使用已有的类型声明文件
- 创建自己的类型声明文件
学习顺序:先会用(别人的)再会写(自己的)