TypeScript介绍
TypeScript是什么
- TypeScript(简称:TS)是JavaScript的超集(JS有的TS都有)
- 在JS的基础上添加了类型支持
- TS是微软开发的开源编程语言,可以在任何运行JS的地方运行
//TS 有明确的类型,即:number(数字类型)
let age1: number = 18
//JS 没有明确的类型
let age2 = 18
TypeScript为什么要添加类型支持
- JS的"动态类型"存在先天缺陷,JS代码中绝大部分错误都是类型错误
- TS属于静态类型的语言,JS属于动态类型的编程语言
- 静态类型:编译期做类型检查;动态类型:执行器做类型检查
- 代码编译和代码执行的顺序:1 编译 2 执行
对于JS来说:需要等到代码真正去执行的时候才能发现错误
对于TS来说:在代码编译的时候(代码执行前)就可以发现错误
TypeScript初体验
安装编译TS的工具包
- 为什么要安装编译TS的工具包?
- Node/浏览器,只认识JS代码,不认识TS代码,需要先将TS代码转为JS代码,然后才能运行
安装命令:npm i -g typescript
typescript包:用来编译TS代码包,提供了tsc命令,实现了TS -> JS的转化
验证是否安装成功: tsc -v(查看ts的版本)
编译并运行TS代码
- 创建hello.ts 文件
- 将TS编译成JS:在终端中输入命令,tsc hello.ts (此时,在同级目录中会出现一个同名的JS文件)
- 执行JS代码:在终端输入:node hello.js
简化运行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常用类型
类型注解
let age: number = 18
说明:代码中的 :number 就是类型注解
作用:为变量添加类型约束。比如,上述代码中,约定变量age的类型为number(数值类型)
解释:约定了什么类型,就只能给变量赋值该类型的值,否则,就会报错
常用基础类型概述
可以将TS中的常用基础类型分为两类:1、JS已有类型 2、TS新增类型
- JS已有类型
- 原始类型:number/string/boolean/null/undefined/symbol
- 对象类型:object(包括数组、对象、函数等对象)
- TS新增类型
- 联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any等
原始类型
- 原始类型:number/string/boolean/null/undefined/symbol
let age: number = 18
let myName: String = "刘老师"
let isLoading: boolean = false
//...
数组类型
- 对象类型:object(包括数组、对象】函数等对象)
- 特点:对象类型,在TS中更加细化,每个具体的对象都有自己的类型语法
数组类型的两种写法:(推荐使用number[]写法)
let number: number[] = [1,3,5]
let strings: Array<String> = ['a','b','c']
需求:数组中既有number类型,又有string类型,这个数组的类型应该怎么写?
let arr: (number | string)[] = [1, 'a', 3, 'b']
解释:一条竖线在TS中叫做联合类型,即由多个类型组成的类型,表示可以是这些类型中的任意一种。
类型别名
- 类型别名(自定义类型):为任意类型起别名
- 使用场景:当同一类型(复杂)被多次使用时,可以通过类型别名,简化该类型的使用
type CustomArray = (number | string)[]
let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]
- 解释:
- 使用type关键字来创建类型别名
- 类型别名(比如CustomArray),可以是任意合法的变量名称
- 之后,直接使用该类型别名作为变量的类型注解即可
函数类型
函数的类型实际上指的是:函数参数和返回值的类型
为函数指定类型的两种方式:
1. 单独指定参数、返回值的类型
function add(num1: number, num2: number): number {
return num1 + num2
}
const add = (num1: number, num2: number): number => {
return num1 + num2
}
2. 同时指定参数、返回值的类型:
const add2: (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 parson: { name: string; age: number; sayHi(): void } = {
name: "ggg",
age: 18,
sayHi() { }
}
解释:
1. 直接使用{}来描述对象结构。属性采用属性名:类型的形式;方法采用方法名():返回值类型的形式
2. 在一行代码中指定对象的多个属性类型时,使用分号来分隔
3. 如果通过换行来分隔多个属性类型,可以去掉分号;
可选属性:对象的属性或方法,也可以是可选的,就用到可选属性了
比如,我们在使用axios({…})时,如果发送GET请求,method属性就可以省略
function myAxios(config: { url: string; method?: string}) {
console.log(config)
}
可选属性的语法与函数可选参数的语法一致,都使用?来表示
接口
当一个对象类型被多次使用时,一般会使用接口来描述对象的类型,达到复用的目的
解释:
1. 使用interface关键字来声明接口
2. 接口名称(比如,此处的IPerson),可以是任意合法的变量名称
3. 声明接口后,直接使用接口名称作为变量的类型
4. 因为每一行只有一个属性类型,因此,属性类型后没有分号
interface IPerson {
name: string
age: number
sayHi(): void
}
let person: IPerson = {
name:'jack',
age: 19,
sayHi() {}
}
接口和类型别名的对比:
- 相同点:都可以给对象指定类型
- 不同点:
- 接口:只能为对象指定类型
- 类型别名:不仅可以为对象指定类型,可以为任意类型指定别名
interface IPerson {
name: string
age: number
sayHi(): void
}
type IPerson = {
name: string
age: number
sayHi(): void
}
- 如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出去,通过继承来实现复用
- 比如,这两个接口都有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的所有属性和方法
元组
场景:在地图中,使用经纬度来标记位置信息
可以使用数组来记录坐标,那么该数组中只有两个元素,并且这两个元素都是数值类型
let position: number[] = [39.5427, 116.2317]
使用number[]的缺点:不严谨,没有限制说只有两个元素
更好的方式: 元组(Tuple)
元组类型是另一种类型的数组,它确切地知道包含多少个元素,以及特定索引对应的类型
let position: [number, number] = [39.5427, 116.2317]
解释:
1. 元组类型可以确切标记有多少个元素,以及每个元素的类型
2. 该示例中,元组有两个元素,每个元素的类型都是number
类型推论
- 在TS中,某些没有明确指出类型的地方,TS的类型推论机制会帮助提供类型
- 换句话说:由于类型推论的存在,某些地方,类型注解可以省略不写
- 发生类型推论的2种场景:
- 声明变量并初始化时
- 决定函数返回值时
注意:这两种情况,类型注解可以不写,提升开发效率
类型断言
有时候你会比TS更加明确一个值的类型,此时,可以使用类型断言来指定更具体的类型
<a href="http://www.itcast.cn/" id="link">传智教育</a>
const aLink = document.getElementById("link")
- 注意:getElementById方法返回的类型是HTMLElement,该类型只包含所有标签公共的属性或方法,不包含a标签特有的href等属性。
因此,这个类型太宽泛,无法操作href等a标签特有的属性或方法 - 解决方法:这种情况就需要使用类型断言指定更加具体的类型
使用类型断言:
const aLink = document.getElementById('link') as HTMLAnchorElement
解释:
1. 使用as关键字实现类型断言
2. 关键字as后面的类型是以后更加具体的类型
3. 通过类型断言,aLink的类型便得更加具体,这样就可以访问a标签的属性和方法了
另一种语法,使用<>语法:
const aLink = <HTMLAnchorElement>document.getElementById('link')
技巧:通过console.dir()打印DOM元素,在属性列表的最后面,即可看到该元素的类型
字面量类型
let str1 = 'Hello TS'
const str2 = 'Hello TS'
通过TS类型推论机制,可以得到答案:
1. 变量str1的类型为:string
2. 变量str2的类型为: ‘Hello TS’
解释:
1. str1是一个常量(let),它的值可以是任意字符串,所以类型为:string
2. str2是一个常量(const),它的值不能变化只能是 ‘Hello TS’,所以它的类型为 ‘Hello TS’
注意:此处的 ‘Hello TS’,就是一个字面量类型。也就是说某个特定的字符串也可以作为TS中的类型
除字符串外,任意的JS字面量(比如,对 象、数字等)都可以作为类型使用
- 使用模式:字面量类型配合联合类型一起使用
- 使用场景:用来表示一组明确的可选值列表
- 比如在贪吃蛇游戏中,游戏的方向的可选值只能是上、下、左、右中的任意一个
function changeDirection(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)
}
解释:
1. 使用enum关键字定义枚举
2. 约定枚举名称、枚举中的值以大写字母开头
3. 枚举中的多个值之前通过,分隔
4. 定义好枚举后,直接使用枚举名称作为类型注解
形参direction的类型为枚举Direction,那么实参的值就应该是枚举Direction成员的任意一个。
访问枚举对象:
changeDirection(Direction.Up)
解释:类似于JS中的对象,直接通过(.)语法访问枚举的成员
问题:我们把枚举成员作为了函数的实参,它的值是什么呢?
解释:通过把鼠标移入Direction.Up,可以看到枚举成员Up的值为0
注意:枚举成员是有值的,默认为:从0开始自增的数值
我们把,枚举成员的值为数字的枚举,称为:数字枚举
当然也可以给枚举中的成员初始化值
enum Direction { Up = 2, Down = 4, Left = 8, Right = 16 }
字符串枚举:枚举成员的值都是字符串
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值
- 枚举是TS为数不多的非JS类型级扩展的特性之一(不仅仅是类型)
- 因为其他类型仅仅被当作类型,而枚举不仅用作类型,还提供值(枚举成员都是有值的)
- 也就是说,其他的类型会在编译为JS代码时自动移除。但是,枚举类型会被编译为JS代码
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
Direction["Left"] = "LEFT";
Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
- 说明:枚举与前面讲到的字面量类型 + 联合类型组合的功能类似,都用来表示一组明确的可选值列表
- 一般情况下,推荐使用字面量类型+联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效。
any类型
- 原则:不推荐使用any!这会失去TS类型保护的优势
- 因为当值的类型为any时,可以对该值进行任意操作,并且不会有代码提示。
let obj: any = { x: 0}
// 访问不存在的属性 或者 赋值
obj.aaa
obj.aaa = 10
// 当作函数调用
obj()
// 赋值给其他类型的变量
let n: number = obj
其他隐式具有any类型的情况:
- 声明变量不提供类型也不提供默认值
- 函数参数不加类型
typeof
- JS中提供了typeof操作符,用来在JS中获取数据的类型
console.log(typeof "Hello world")
- TS也提供了typeof操作符:可以在类型上下文中引用变量或属性的类型(类型查询)
- 使用场景:根据已有变量的值,获取该值的类型,来简化类型书写
let p = { x: 1, y: 2}
function formatPoint(point: {x: number; y: number}) {}
function formatPoint2(point: typeof p){}
TypeScript高级类型
class类
- TypeScript全面支持ES2015中引入的class关键字,并为其添加了类型注解和其他语法(比如,可见性修饰符等)
class Person{}
const p = new Person()
解释:
1. 根据TS中的类型推论,可以知道Person类的实例对象p的类型是Person
2. TS中的class,不仅提供了class的语法功能,也作为一种类型存在
实例属性初始化:
class Person {
age: number
gender = '男'
}
解释:
1. 声明成员age,类型为number(没有初始值)
2. 声明成员gender,并设置初始值,此时,可省略类型注解(TS类型推论为string类型)
构造函数:
class Person {
age: number
gender: string
constructor(age: number, gender: string) {
this.age = age
this.gender = gender
}
}
解释:
1. 成员初始化(比如,age:number)后,才可以通过this.age来访问实例成员
2. 需要为构造函数指定类型注解,否则会被隐式推断为any;构造函数不需要返回值类型
实例方法:
class Point{
x = 10
y = 10
scale(n: number): void {
this.x *= n
this.y *= n
}
}
解释:方法的类型注解(参数和返回值)与函数用法相同
类继承的两种方式:
1. extends(继承父类)
2. implements(实现接口)
说明:JS中只有extends,而implements是TS提供的
class Animal {
move() {
console.log("animal")
}
}
class Dog extends Animal {
bark() {
console.log("dog")
}
}
const dog = new Dog()
dog.move()
dog.bark()
解释:
1. 通过extends关键字实现继承
2. 子类就获得了父类的所有属性和方法
实现接口:
interface Singable {
sing(): void
}
class Person implements Singable {
sing() {
console.log("implements");
}
}
解释:
1. 通过implements关键字让class实现接口
2. Person类实现接口Singable意味着,Person类中必须提供Singable接口中指定的所有方法和属性
类成员的可见性:可以使用TS来控制class的方法或属性对于class外的代码是否可见
- 可见性修饰符包括:
- public(公有的)
- protected(受保护的)
- private(私有的)
- public:表示公有的,公有成员可以被任何地方访问,默认可见性
class Animal {
public move() {}
}
解释:
1. 在类属性或者方法前面添加public关键字,来修饰属性或方法是共有的
2. 因为public是默认可见的,所以可以直接省略
- protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见
class Animal {
protected move() {
console.log("animal")
}
}
class Dog extends Animal {
bark() {
console.log("dog")
this.move()
}
}
解释:
1. 在类属性或方法前面添加protected关键字,来修饰该属性或方法是受保护的
2. 在子类的方法内部可以通过this来访问父类中受保护的成员,但是对实例不可见
private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的
class Animal {
private move() {
console.log("animal")
}
}
解释:
1. 在类属性或者方法前面添加private关键字,来修饰该属性或方法是私有的
2. 私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的
除了可见性修饰符之外,还有一个常见修饰符就是:readonly(只读修饰符)
readonly:表示只读,用来防止在构造函数之外对属性进行赋值
class Person {
readonly age: number = 18
constructor(age: number) {
this.age = age
}
}
解释:
1. 使用readonly关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法
2. 属性age如果不加类型注解,则age的类型为字面量类型18
3. 接口或者{}表示的对象类型,也可以使用readonly
类型兼容性
- 两种类型系统:1 结构化类型系统 2 标明类型系统
- 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. 但是在标明类型系统中,比如C#、Java等,它们是不同的类,类型无法兼容
- 在结构化类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型,这种说法并不准确。
- 更准确说法:对于对象类型来说,y的成员至少与x相同,则x兼容y**(成员多的可以赋值给少的)**
class Point { x: number; y: number }
class Point3D { x: number; y: number; z: number}
const p: Point = new Point3D()
解释:
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 }
let p1: Point = { x: 10, y: 10 }
let p2: Point2D = p1
interface Point3D { x: number; y: number; z: number }
let p3: Point3D = { x: 10, y: 10, z: 10 }
p2 = p3
- 函数之间的兼容性比较复杂,需要考虑:1 参数个数 2 参数类型 3 返回类型
- 参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)
type F1 = (a: number) => void
type F2 = (a: number, b: number) => void
let f1: F1 = function (a){}
let f2: F2 = f1
为什么这样规定?
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的类型
参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)
type F1 = (a: number) => void
type F2 = (a: number, b: number) => void
let f1: F1 = function (a){}
let f2: F2 = f1
解释:函数类型F2兼容函数类型F1,因为F1和F2的第一个参数类型相同
interface Point2D { x: number; y: number}
interface Point3D { x: number; y: number; z: number}
type F2 = (p: Point2D) => void
type F3 = (p: Point3D) => void
let f2: F2 = () =>{}
let f3: F3 = 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 = () => ({ name: "", age: 10 })
f7 = f8
解释:
1. 如果返回值类型是原始类型,此时两个类型要相同,比如,类型F5和F6
2. 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如F7和F8
交叉类型
交叉类型(&):功能类似于接口继承,用于组合多个类型为一个类型(常用于对象类型)
interface Person { name: string }
interface Contact { phone: string }
type PersonDetail = Person & Contact
let obj: PersonDetail = {
name:'jack',
phone:'133.....'
}
解释:使用交叉类型后,新的类型PersonDetail就同时具备了Person和Contact的所有属性类型。
相当于:
type PersonDetail = { 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
泛型
- 泛型是可以保证类型安全的前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数、接口、class中
需求:创建一个id函数,传入什么数据就返回数据本身(也就是说,参数和返回值类型相同)
function id(value: number): number { return value }
- 比如,id(10)调用以上函数就会直接返回10本身。但是,该函数只接受数值类型,无法用于其他类型。
- 为了能让函数能够接受任意类型,可以将参数类型修改为any。但是这样就失去了TS的保护,类型不安全。
function id(value: any): any { return value }
泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等与多种不同的类型一起工作,灵活可复用。
创建泛型函数:
function id<Type>(value: Type): Type { return value }
解释:
1. 语法:在函数名称后面添加<>(尖括号),尖括号中添加类型变量,比如此处的Type
2. 类型变量Type,是一种特殊类型的变量,处理类型而不是值
3. 该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)
4. 因为Type是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型
5. 类型变量Type,可以是任意合法的变量名称
调用泛型函数:
function id<Type>(value: Type): Type { return value }
const num = id<number>(10)
const str = id<string>('a')
console.log(num,str)
解释:
1. 语法:在函数名称的后面添加<>(尖括号),尖括号中指定具体的类型,比如,此处的number
2. 当传入类型number后,这个类型就会被函数指定的类型变量Type捕获到
3. 此时,Type的类型就是number,函数id参数和返回值的类型就都是number
这样,通过泛型就做到了让id函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全
简化调用泛型函数:
function id<Type>(value: Type): Type { return value }
const num = id(10)
const str = id('a')
console.log(num,str)
解释:
1. 在调用泛型函数时,可以省略<类型>来简化泛型函数的调用
2. 此时,TS内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量Type的类型
3. 比如,传入实参10,TS会自动推断出变量num的类型number,并作为Type的类型
推荐:使用这种简化的方式调用泛型函数,使代码更短,更易于阅读
泛型约束:默认情况下,泛型函数的类型变量Type可以代表多个类型,这导致无法访问任何属性
比如,id(‘a’)调用函数时获取参数的长度
function id<Type>(value: Type): Type {
// 报错
console.log(value.length)
return value
}
解释:Type可以代表任何类型,无法保证一定存在length属性,比如number类型就没有length。
此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)
添加泛型约束收缩类型,主要有以下两种方式:
- 指定更加具体的类型
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 }
console.log(getProp(person, 'name'));
解释:
1. 添加了第二个类型变量Key,两个类型变量之间使用逗号分隔。
2. keyof关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型
3. 本示例中keyof Type实际上获取的是person对象所有键的联合类型,也就是:‘name’ | ‘age’
4. 类型变量Key受Type约束,可以理解为: Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性
泛型接口:接口也可以配合泛型来使用,以增加其灵活性,增强其复用性
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. 使用泛型接口时,需要显示指定具体的类型(没有类型推断)
4. 此时,id方法的参数的返回值类型都是number;ids方法的返回值类型是number[]
实际上,JS中的数组在TS中就是一个泛型接口
const strs = ['a', 'b', 'c']
strs.forEach
const nums = [1, 3, 5]
nums.forEach
解释:当我们在使用数组时,TS会根据数组的不同类型,来自动将类型变量设置为相应的类型
技巧:可以通过Ctrl + 左键来查看具体的类型信息
泛型类: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中的一些常见操作
- 说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用
- 这种工具类型很多,主要学习以下几个:
- Partial
- Readonly
- Pick<Type, Keys>
- Record<Keys, Type>
Partial用来构造一个类型,将Type的所有属性设置为可选
interface Props {
id:string
children:number[]
}
type PertialProps = Partial<Props>
解释:构造出来的新类型PertialProps结构和Props相同,但所有属性都变为可选的
Readonly用来构造一个类型,将Type的所有属性都设置为readonly(只读)
interface Props {
id:string
children:number[]
}
type ReadonlyProps = Readonly<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: ['1'],
c: ['1'],
}
解释:
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中就可以出现任意多个属性
3. key只是一个占位符,可以换成任意合法的变量名称
在JS中数组是一类特殊的对象,特殊在数组的键(索引)是数值类型
并且,数组也可以出现任意多个元素。所以,在数组对应的泛型接口中,也用到了索引签名类型
interface MyArray<T>{
[n: number]: T
}
let arr: MyArray<number> = [1, 3, 5]
解释:
1. MyArray接口模拟原生的数组接口,并使用[n: number]来作为索引签名类型
2. 该索引签名类型表示:只要是number类型的键(索引)都可以出现在数组中,或者说数组中可以有任意个元素
3. 同时也符合数组索引是number类型这一前提
映射类型
映射类型:基于旧类型创建新类型(对象类型),减少重复,提升开发效率
比如:类型PropKeys有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(ket k in obj)
3. 使用映射类型创建的新对象类型Type2和类型Type1结构完全相同
4. 映射类型只能在类型别名中使用,不能在接口中使用
映射类型除了根据联合类型创建新类型外,还可以根据对象类型来创建:
type Props = { a: number; b: string; c: boolean }
type Type3 = { [key in keyof Props ]: number }
解释:
- 首先,先执行keyof Props获取到对象类型Props中所有键的联合类型即,’a’|‘b’|‘c’
- 然后根据Key in…就表示Key可以是Props中所有的键名称中的任意一个
实际上,前面讲到的泛型工具类型(比如,Partial)都是基于映射类型实现的
比如,Partial的实现:
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
type Props = { a: number; b: string; c: boolean }
type PartialProps = MyPartial<Props>
let obj: PartialProps = {
a:10
}
解释:
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; b: string; c: boolean }
type TypeA = Props['a']
解释:Props[‘a’]表示查询类型Props中属性‘a’对应的类型number。所以,TypeA的类型为number
注意:[]中的属性必须存在于被查询类型中,否则就会报错
索引查询类型的其他使用方式:同时查询多个索引的类型
type Props = { a: number; b: string; 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类型声明文件
概述
- 今天几乎所有的JS应用都会引入许多第三方库来完成任务需求
- 这些第三方库不管是否是用TS编写的,最终都要编译成JS代码,才能发布给开发者
- 我们知道是TS提供了类型,才有了代码提示和类型保护等机制
- 但在项目开发中使用第三方库时,你会发现他们几乎都有相应的TS类型,这些类型是怎么来的?类型声明文件
- 类型声明文件:用来为已存在的JS库提供类型信息
- 这样在TS项目中使用这些库时,就像用TS一样,都会有代码提示、类型保护等机制了
- TS的两种文件类型
- 类型声明文件的使用说明
TS中的两种文件类型
TS中有两种文件类型:1 .ts文件 2 .d.ts文件
- .ts文件
- 既包含类型信息又可执行代码
- 可以被编译为.js文件,然后,执行代码
- 用途:编写程序代码的地方
- .d.ts文件:
- 只包含类型信息的类型声明文件
- 不会生成.js文件,仅用于提供类型信息
- 用途:为JS提供类型信息
- 总结:.ts时implementation(代码实现文件);.d.ts是declaration(类型声明文件)
- 如果要为JS库提供类型信息,要使用.d.ts文件
类型声明文件的使用说明
在使用TS开发项目时,类型声明文件的使用包括以下两种方式:
- 使用已有的类型声明文件
- 创建自己的类型声明文件
学习顺序:先会用(别人的) 再会写(自己的)
- 使用已有的类型声明文件:1 内置类型声明文件 2 第三方库的类型声明文件
- 内置类型声明文件:TS为JS运行时可用的所有标准化内置API都提供了声明文件
比如:在使用数组时,数组所有方法都会有相应的代码提示以及类型信息:
(method) Array<number>.forEach(callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any): void
实际上这都是TS提供的内置类型声明文件
可以通过ctrl + 鼠标左键来查看内置类型声明文件内容
比如,查看forEach方法的类型声明,在VSCode中会自动跳转到lib.es5.d.ts类型声明文件中
第三方库的类型声明文件:目前,几乎所有常用的第三方库都有相应的类型声明文件
第三方库的类型声明文件有两种存在形式:1 库自带类型声明文件 2 由 DefinitelyTyped提供
4. 库自带类型声明文件
解释:这种情况下,正常导入该库,TS就会自动加载库自己的类型声明文件,以提供该库的类型声明
5. 由DefinitelyTyped提供
- DefinitelyTyped是一个github仓库,用来提供高质量TypeScript类型声明
- 可以通过npm/yarn来下载该仓库提供的TS类型声明包,这些包的名称格式为:@types/*
- 比如,@types/react、@types/lodash等
- 说明:在实际项目开发时,如果你使用的第三方库没有自带的声明文件,VSCode会给出明确的提示
解释:当安装@types/*类型声明包后,TS会自动加载该类型声明包,已提供该库的类型声明
补充:TS官方文档提供了一个页面,可以来查询@types/库
- 创建自己的类型声明文件:1 项目内共享类型 2 为已有JS文件提供类型声明
-
项目内共享类型:如果多个.ts文件中都用到同一个类型,此时可以创建.d.ts文件提供该类型,实现类型共享
操作步骤:
1. 创建index.d.ts类型声明文件
2. 创建需要共享的类型,并使用export导出(TS中的类型也可以使用import/export实现模块化功能)
3. 在需要使用共享类型的.ts文件中,通过import导入即可(.d.ts后缀导入时,直接省略). -
为已有的JS文件提供类型声明:
1. 在将JS项目迁移到TS项目时,为了让已有的.js文件有类型声明
2. 成为库作者,创建库给其他人用
注意:类型声明文件的编写与模块化方式有关,不同的模块化方式有不同的写法。
但由于历史原因,JS模块化的发展经历过多种变化(AMD、CommonJS、UMD、ESModule等),而TS支持各种模块化形式的类型声明。这就导致,类型声明文件相关内容又多又杂。
演示:基于最新的ESModule(import/export)来为已有.js文件,创建类型声明文件
开发环境准备:使用webpack搭建,通过ts-loader处理.ts文件
-
在React中使用TypeScript
概述
现在,我们已经掌握了TS中基础类型、高级类型的使用了。但是如果要在前端项目开发中使用TS,还需要React、Vue、Angular等这些库或者框架中提供的API的类型,以及在TS中是如何使用的。
接下来,我们以React为例,来学习如何在React项目中使用TS。包括以下内容:
- 使用CRA创建支持TS的项目
- TS配置文件tsconfig.json
- React中的常用类型
使用CRA创建支持TS的项目
- React脚手架工具create–react-app(简称:CRA)默认支持TypeScript
- 创建支持TS的项目命令:npx create-react-app 项目名称 – template typescript
使用CRA创建支持TS的项目
相对于非TS项目,目录结构主要有以下三个变化:
- 项目根目录中增加了tsconfig.json配置文件:**指定TS的编译选项(比如,编译时是否移除注释)
- React组件的文件扩展名变为:*.tsx
- src目录中增加了react-app-env.d.ts:React项目默认的类型声明文件
react-app-env.d.ts:React项目默认的类型声明文件
三斜线指令:指定依赖的其他类型声明文件,types表示依赖的类型声明文件包的名称
/// <reference types="react-scripts" />
解释:告诉TS帮我加载react-scripts这个包提供的类型声明
- react-scripts的类型声明文件包含了两部分类型:
1. react、react-dom、node的类型
2. 图片、样式等模块的类型,以允许在代码中导入图片、SVG等文件
TS会自动加载该.d.ts文件,已提供类型声明(通过修改tsconfig.json中的include配置来验证)
TS配置文件tsconfig.json
tsconfig.json 指定:项目文件和项目编译所需的配置项
注意:TS的配置项非常多(100+),以CRA项目中的配置为例来学习,其他的配置项用到时查文档即可
- tsconfig.json 文件所在目录为项目根目录(与package.json同级)
- tsconfig.json 可以自动生成,命令:tsc–init
{
// 编译选项
"compilerOptions": {
// 生成代码的语言版本
"target": "es5",
// 指定要包含在编译中的library
"lib": [
"dom",
"dom.iterable",
"esnext"
],
// 允许ts编译器编译js文件
"allowJs": true,
// 跳过声明文件的类型检查
"skipLibCheck": true,
// es模块 互操作,屏蔽 ESModule 和 CommonJS 之间的差异
"esModuleInterop": true,
// 允许通过import x from 'y' 即使模块没有显示指定default导出
"allowSyntheticDefaultImports": true,
// 开启严格模式
"strict": true,
// 对文件名称强制区分大小写
"forceConsistentCasingInFileNames": true,
// 为switch 语句启用错误报错
"noFallthroughCasesInSwitch": true,
// 生成代码的模块化标准
"module": "esnext",
// 模块解析(查找)策略
"moduleResolution": "node",
// 允许导入扩展名为.json的模块
"resolveJsonModule": true,
// 是否将没有import/export的文件视为旧(全局而非模块化)脚本文件
"isolatedModules": true,
// 编译时不生成任何文件(只进行类型检查)
"noEmit": true,
// 指定将JSX编译成什么形式
"jsx": "react-jsx"
},
// 指定允许ts处理的目录
"include": [
"src"
]
}
- 除了在tsconfig.json文件中使用编译配置外,还可以通过命令行来使用
- 使用演示:tsc hello.ts --target es6
- 注意:
1. tsc后带有输入文件时(比如tsc hello.ts),将忽略tsconfig.json文件
2. tsc后不带输入文件时(比如,tsc),才会启用tsconfig.json - 推荐使用:tsconfig.json配置文件
React中的常用类型
- 前提说明:现在,基于class组件来讲解React+TS的使用(函数组件后面讲解)
- 在不使用TS时,可以使用prop-types库,为React组件提供类型检查
- 说明:TS项目中,推荐使用TypeScript实现组件类型校验(代替PropTypes)
- 不管是React还是Vue,只要支持TS的库,都提供了很多类型,来满足该库对类型的需求
注意:
1. React项目是通过@types/react、@types/react-dom类型声明包,来提供类型的3
2. 这些包CRA已帮我们安装好(react-app-env-.d.ts),直接用即可
React是组件化开发模式,React开发主要任务就是写组件,两个组件:1 函数组件 2 class组件
函数组件中的常见类型
- 函数组件,主要包括以下内容:
* 组件的类型
* 组件的属性(props)
* 组件属性的默认值(defaultProps)
* 事件绑定和事件对象
- 函数组件的类型以及组件的属性
type Props = { name: string; age?: number }
const Hello: FC<Props> = ({ name, age } ) => (
<div>
你好,我是{name}
</div>
)
const App = () => (
<div>
<Hello name = "jack" />
</div>
)
实际上,还可以直接简化为(完全按照函数在TS中的写法)
const Hello = ({ name, age }: Props) => (
<div>你好,我是{name}</div>
)
- 函数组件属性的默认值(defaultProps)
const Hello: FC<Props> = ({ name, age }) => (
<div>{name}</div>
)
Hello.defaultProps = {
age: 18
}
实际上,还可以直接简化为(完全按照函数在TS中的写法)
const Hello = ({ name, age = 18 }: Props) => (
<div>{name},{age} </div>
)
- 事件绑定和事件对象
<button onClick={onClick}>点赞</button>
const onClick = () => {}
const onClick1 = (e: React.MouseEvent<HTMLButtonElement>) => {}
再比如,文本框:
<input onChange={onChange} />
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {}
技巧:在JSX中写事件处理程序(e => {} ),然后,把鼠标放在e上,利用TS的类型推论来查看事件对象类型
class组件中的常用类型
class组件中,主要包括以下内容:
- 组件的类型、属性、事件
- 组件状态(state)
class组件的类型
type State = { count: number }
type Props = { message?: string }
class C1 extends React.Component {} //无props、state
class C2 extends React.Component<Props> {}//有props、无state
class C3 extends React.Component <{}, State>{} //无props、有state
class C1 extends React.Component<Props, State> {} //有props、state
- class组件的属性和属性默认值
type Props = { name: string; age?: number }
class Hello extends React.Component<Props> {
static defaultProps: Partial<Props> = {
age: 18
}
render() {
const { name, age } = this.props
return <div>{name}</div>
}
}
const { name, age = 18 } = this.props
- class组件状态(state)和事件
type State = { count: number }
class Counter extends React.Component<{}, State> {
state: State = {
count: 0
}
onIncrement = () => {
this.setState({
count: this.state.count + 1
})
}
<button onClick={this.onIncrement}>+1</button>