TypeScript学习

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代码

  1. 创建hello.ts 文件
  2. 将TS编译成JS:在终端中输入命令,tsc hello.ts (此时,在同级目录中会出现一个同名的JS文件)
  3. 执行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新增类型

  1. JS已有类型
    • 原始类型:number/string/boolean/null/undefined/symbol
    • 对象类型:object(包括数组、对象、函数等对象)
  2. 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]
  • 解释:
    1. 使用type关键字来创建类型别名
    2. 类型别名(比如CustomArray),可以是任意合法的变量名称
    3. 之后,直接使用该类型别名作为变量的类型注解即可

函数类型

函数的类型实际上指的是:函数参数返回值的类型
为函数指定类型的两种方式:
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}

解释:

  1. 使用extends关键字实现了接口的继承
  2. 继承之后,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种场景:
    1. 声明变量并初始化时
    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类型的情况:

  1. 声明变量不提供类型也不提供默认值
  2. 函数参数不加类型

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外的代码是否可见

  • 可见性修饰符包括:
    1. public(公有的)
    2. protected(受保护的)
    3. 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 返回类型
  1. 参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)
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。
此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)


添加泛型约束收缩类型,主要有以下两种方式:

  1. 指定更加具体的类型
function id<Type>(value: Type[]): Type[] {
    console.log(value.length);
    return value
}

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

  1. 添加约束
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中的一些常见操作

  • 说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用
  • 这种工具类型很多,主要学习以下几个:
    1. Partial
    2. Readonly
    3. Pick<Type, Keys>
    4. 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 }

解释:

  1. 首先,先执行keyof Props获取到对象类型Props中所有键的联合类型即,’a’|‘b’|‘c’
  2. 然后根据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一样,都会有代码提示、类型保护等机制了
  1. TS的两种文件类型
  2. 类型声明文件的使用说明

TS中的两种文件类型

TS中有两种文件类型:1 .ts文件 2 .d.ts文件

  • .ts文件
    1. 既包含类型信息又可执行代码
    2. 可以被编译为.js文件,然后,执行代码
    3. 用途:编写程序代码的地方
  • .d.ts文件:
    1. 只包含类型信息的类型声明文件
    2. 不会生成.js文件,仅用于提供类型信息
    3. 用途:为JS提供类型信息
  • 总结:.ts时implementation(代码实现文件);.d.ts是declaration(类型声明文件)
  • 如果要为JS库提供类型信息,要使用.d.ts文件

类型声明文件的使用说明

在使用TS开发项目时,类型声明文件的使用包括以下两种方式:

  1. 使用已有的类型声明文件
  2. 创建自己的类型声明文件
    学习顺序:先会用(别人的) 再会写(自己的)

  • 使用已有的类型声明文件:1 内置类型声明文件 2 第三方库的类型声明文件
  1. 内置类型声明文件: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文件提供类型声明
    1. 项目内共享类型:如果多个.ts文件中都用到同一个类型,此时可以创建.d.ts文件提供该类型,实现类型共享
      操作步骤:
      1. 创建index.d.ts类型声明文件
      2. 创建需要共享的类型,并使用export导出(TS中的类型也可以使用import/export实现模块化功能)
      3. 在需要使用共享类型的.ts文件中,通过import导入即可(.d.ts后缀导入时,直接省略).

    2. 为已有的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。包括以下内容:

  1. 使用CRA创建支持TS的项目
  2. TS配置文件tsconfig.json
  3. React中的常用类型

使用CRA创建支持TS的项目

  • React脚手架工具create–react-app(简称:CRA)默认支持TypeScript
  • 创建支持TS的项目命令:npx create-react-app 项目名称 – template typescript

使用CRA创建支持TS的项目

相对于非TS项目,目录结构主要有以下三个变化:

  1. 项目根目录中增加了tsconfig.json配置文件:**指定TS的编译选项(比如,编译时是否移除注释)
  2. React组件的文件扩展名变为:*.tsx
  3. 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项目中的配置为例来学习,其他的配置项用到时查文档即可

  1. tsconfig.json 文件所在目录为项目根目录(与package.json同级)
  2. 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组件

函数组件中的常见类型

  1. 函数组件,主要包括以下内容:
    * 组件的类型
    * 组件的属性(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>
  • 48
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值