TypeScript零基础入门到实战全套教程

475 阅读16分钟

1.TypeScript准备

type+javascript TypeScript是JavaScript的超集:js有的ts都有 image.png

1.1 为什么要为JS添加类型支持

JS的系统存在先天缺陷,JS代码中绝大部分错误都是“Uncaught TypeError”。 问题:增加了找bug,改bug的时间,严重影响开发效率

代码先编译,后执行 TypeScript:静态类型语言,在编译期做类型检查。代码在编译(执行前)就发现错误。 JavaScript:动态型语言,在执行期做类型检查。代码真正执行的时候去发现错误 image.png

1.2 安装TypeScript

Node和浏览器不能直接运行TS,需要把TS转化成JS。

  • 实现TypeScript向JavaScript的转变
npm i typescript -g    #全局安装TypeScript
tsc -v     #查看版本
  
tsc hello.ts    #编译ts文件:生成js文件
node hello.js    #执行js代码
  • 简化运行TS的步骤

ts-node可以编译和执行ts文件

npm i -g ts-node  #安装ts-node包
ts-node hello.ts   #将TS转化成JS,然后运行JS代码

2.TS基础类型

2.1 原始类型

number、string、boolean、undefined、null、symbol 冒号后面的内容叫:类型注解

let username: string = "张老师";
let age: number = 21;
let isLoading: boolean = true
let a: null = null;
let b: undefined = undefined;
let c: symbol = Symbol();

2.2 数组类型

//数组类型
let numbers: number[] = [100, 2003, 2020]
let numbers1: Array<number> = [2020, 2021, 2022]

//联合类型
let arr: (number | string)[] = [2002, 20003, "北京", "上海"]
let arr1: number | string[] = 2002

2.3 类型别名

相同的类型反复使用,这时候

type typeArray = (number | string)[]
let arr1: typeArray = [2002, 2003]
let arr2: typeArray = ["北京", "上海"]

2.4 函数类型

给函数添加类型,实际给参数和返回值添加类型

//普通函数
function fn(num1: number, num2: number): number {
  return num1 + num2
}

//箭头函数
const fn1=(num1: number, num2: number): number => {
  return num1 + num2
}

//同时指定参数和返回值类型
const fn3: (num1: number, num2: number) => number = (num1, num2) => {
  // return 100
  return num1 + num2
}
console.log(fn3(100, 200))

//void类型:没有返回值类型
const fn4 = function (username: string): void {
  console.log("hello" + username)
}

//可选参数
function mySlice(start?: number, end?: number) {
  console.log('起始索引', start, '结束索引', end)
}

mySlice()
mySlice(10)
mySlice(10, 20)

2.5 对象类型

//写在同一行
let person: { name: string; age: number; say(): void } = { name: "张三", age: 21, say() { } }

//换行
let person1: {
  name: string,
  age: number,
  say(): void
  greet(name: string): void
} = {
  name: "张三",
  age: 21,
  say() {
    console.log("北京欢迎你")
  },
  greet() {
    console.log("我在打招呼")
  }
}
  • 可选类型
//对象的可选类型
function myAxios(config: { url: string, method?: string }) {

}
myAxios({
  url: "http://www.baidu.com"
})
  • 接口类型

当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的。

//接口类型
interface IPerson {
  name: string,
  age: number,
  say(): void
}


let person: IPerson = {
  name: "张三",
  age: 19,
  say() {
    console.log("我正在说话")
  }
}
  • 类型别名和接口的区别:
    • 接口,只能为对象指定类型
    • 类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名。
	//接口类型
type IPerson = {
  name: string,
  age: number,
  say(): void
}

//类型别名
let person: IPerson = {
  name: "张三",
  age: 19,
  say() {
    console.log("我正在说话")
  }
}
  • 接口的继承

新接口继承了旧接口中的所有属性和方法

//接口的继承
interface point2D {
  x: number,
  y: number
}

interface point3D extends point2D {
  z: number
}

const p3: point3D = {
  x: 100,
  y: 100,
  z: 100
}

2.6 元组

元组是另一种类型的数组,是明确元素数量和类型的数组

//元组
let position: [number, number] = [140, 42]

2.7 类型推断

能省略类型注解的地方则省略,尽量使用ts的类型推断,这样能提高开发效率

//age的类型自动为string
let age = 100;
age = "100"   //ts会检测出错误

//函数的返回值没有指定类型,自己推论为number
function sum(num1: number, num2: number) {
  return num1 + num2
}

2.8 字面量

使用模式:字面量类型配合联合类型一起使用 使用场景:用来表示一组明确的可选值列表 比如:贪吃蛇游戏,方向的可选值是上、下、左、右。

//字面量类型
function changeDirection(direction: 'up' | 'down' | 'left' | 'right') {
  console.log(direction)
}

优势:相比于string类型,使用字面量更加精确、严谨

2.9 枚举类型

2.9.1 数字枚举

默认从0自增,也可以设定初始值,然后依次自增

//枚举
enum Direction { Up=10, Down, Left, Right }
function changeDirection(direction: Direction) {
  console.log(direction)
}

changeDirection(Direction.Left)    //打印:12

类似于js中的对象,直接使用点(.)语法访问枚举的成员。

2.9.2 字符串枚举

字符串枚举必须指定默认值

enum Direction {
  up = 'up',
  down = 'down',
  left = 'left',
  right = 'right'
}

function changeDirection(direction: Direction) {
  console.log(direction)
}
changeDirection(Direction.up)   //打印:up

2.10 any类型

不推荐使用any!这会让typeScript编程“AnyScript”,失去TS类型保护的优势

let obj: any = { x: 0 }

obj.bar = 100
obj()
const n: number = obj

//声明的时候没有指定类型,默认就是any类型
let a
a = 1;
a = ''
a()

//函数参数没有指定类型,默认就是any类型
function add(num1, num2) {

}

add(1, 2)
add(1, '2')
add(1, false)

2.11 typeof检测类型

检测变量或对象属性的类型

  • 在类型注解中,获取ts的类型
  • 在普通js中,获取js的类型
//js中的类型检测
console.log(typeof("北京"))

//ts的类型检测:在类型注解中,获取x和y的类型
let p = { x: 1, y: 2 }
function getPoint(point: typeof p) {
  return point.x + point.y
}

let result = getPoint({ x: 1, y: 100 })
console.log(result)

//会报错,不能获取函数返回值的类型
let ret: typeof getPoint({ x: 1, y: 100 })

3.TS高级类型

3.1 class类

  • 构造函数的类型约束
class Person {
  //添加实例属性age和name
  name: string = '张三'
  age: number

  //构造函数没有返回值类型
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age
  }

}

let p = new Person("张三", 21)
console.log(p.name)
console.log(p.age)
  • 普通方法的类型约束
class Point {
  x = 10;
  y = 10

  scale(n: number) {
    this.x *= n;
    this.y *= n
  }
}

let p = new Point()
p.scale(4)
console.log(p.x, p.y)

3.2 类的继承

//类的继承
class Animal {
  move() {
    console.log("我能跑步")
  }

}

class Dog extends Animal {
  bark() {
    console.log("汪汪")
  }
}

const dog = new Dog()
dog.move()
dog.bark()

3.3 类实现接口

//类实现接口
interface Singable {
  sing(): void
}

//Person类中必须实现Singable中实现的sing方法
class Person implements Singable {
  sing(): void {
    console.log("你是我的小呀小苹果")
  }
}

3.4 public、protected和private

  • public:对当前类、子类和实例对象都可见
  • protected:在当前类和子类可见,对实例对象不可见
  • private:只在当前类中可见,对实例对象和子类不可见

//类的可见性
class Animal {
  private __run__() {
    console.log('Animal内部辅助函数')
  }

  protected move() {
    this.__run__()
    console.log("走两步")
  }

  public run() {
    this.__run__()
    this.move()
    console.log("跑起来")
  }
}

class Dog extends Animal {
  bark(){
    console.log("汪汪")
  }
}

let d=new Dog();
d.run()

3.5 readonly

只能在constructor中赋值,不能在其他方法中赋值 只能修饰属性,不能修饰方法

class Person {
  readonly username: string;

  constructor(username: string) {
    this.username = username
  }


  setName(name) {
    this.username = name    //报错:不能为只读属性赋值

  }
}
  • 对象也可以使用只读属性
//对象也可以使用只读属性
let obj: { readonly name: string } = {
  name: "jack"
}

obj.name = "david"      //不能为只读属性赋值

3.6 类型兼容性

两种类型系统

  • 结构化类型系统:关注值所具有的形状
  • 标明性类型系统
class Point { x: number; y: number }
class Point2D { x: number; y: number }
class Point3D { x: number; y: number; z: number }

const p: Point = new Point2D()

//成员多的可以赋值给成员少的
const p1: Point = new Point3D() 

3.6.1 interface兼容性

成员多的可以赋值给成员少的

3.6.2 函数参数兼容性

1.简单的

//参数少的可以赋值给参数多的
type F1 = (a: number) => void
type F2 = (a: number, b: number) => void

let f1: F1
let f2: F2
// f2 = f1
// f1 = f2
f2 = f1			//参数
// f1 = f2   //参数少的可以赋值给参数多的

2.复杂的

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

f3 = f2    //函数中参数少的可以赋值给参数多的

3.6.3 函数返回值兼容性

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

//原始类型
type F5 = () => string
type F6 = () => string

let f5: F5
let f6: F6


//对象类型
type F7 = () => { name: string }
type F8 = () => { name: string, age: number }

let f7: F7
let f8: F8
f7 = f8

3.7 交叉类型

交叉类型(&),用于组合多个类型为1个类型,常用语对象类型

//交叉类型
interface Person {
  name: string
  say(): number
}

interface Contact {
  phone: string
}

type PersonDetail = Person & Contact
let obj: PersonDetail = {
  name: "张三",
  phone: "18301682292",
  say() {
    return 1
  }
}

交叉类型和接口继承的对比: 相同:都可以实现对象类型的组合 不同:对于同名属性,处理类型冲突的方式不同

// 1.使用继承,会报错
interface A {
  fn: (value: number) => string
}

interface B extends A {     
  fn: (value: string) => string
}

//2.使用&,不会报错
interface A {
  fn: (value: number) => string
}

interface B {
  fn: (value: string) => string
}

type C = A & B

let c:C
c.fn(100)
c.fn("张三")			//传递number和string都可以


3.8 泛型

什么时候用泛型?让接口、函数、变量、参数等等,既具有类型多样性,又要保证类型安全,这时候就用泛型

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

const num = id<number>(10)
const str = id<string>("你好")
const ret = id<boolean>(true)

解释:

  1. 语法:在函数名称后面添加<>(尖括号),尖括号中指定具体的类型,比如此处的number
  2. 当传入number后,这个类型就会被函数声明时指定的类型变量Type捕获到
  3. 此时,Type的类型就是number,所以函数id的参数和返回值类型也都是number

同样,如果传入类型string,函数id参数和返回值的类型都是string 同样,通过泛型就做到了让id函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全

3.8.1 泛型中类型推断

简化调用泛型函数

function id<Type>(value: Type) { value }
const num = id(100)
const str = id("北京你好")

解释:

  1. 在调用泛型函数的时候,可以省略<类型>来简化函数的调用
  2. 此时,TS内部会蚕蛹一种叫做类型参数推断的机制,根据实参的类型自动推断出类型变量Type的类型
  3. 比如,传入实参10,TS会自动推断出num的类型number,并作为Type的类型
  4. 当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数

3.8.2 类型约束

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

  1. 指定更加具体的类型

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

//添加泛型的类型约束
function id<Type>(value: Type[]): Type[] {
  return value
}

  1. 添加约束
//添加约束
interface ILength { length: number }

//Type满足Ilength的类型约束
function id<Type extends ILength>(value: Type) {
  console.log(value.length)
  return value
}
id(["a","b"])   //数组有length属性
id("北京你好")   //字符串有length属性

解释:

  1. 创建描述约束的接口ILength,该接口要求提供length属性
  2. 通过extends关键字使用该接口,为泛型(类型变量)添加约束
  3. 该约束表示:传入的类型必须具有length属性
  4. 传入的实参只要有length属性即可,这也符合前面降到的类型兼容性

3.8.3 对类型变量约束

泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)。 创建一个函数涵获取对象中属性的值:

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|age”
  4. 类型变量Key受Type约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性

3.8.4 泛型接口

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

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

解释:

  1. 在接口名称的后面添加<类型变量>,name,这个接口就变成了泛型接口
  2. 接口的类型变量,对接口中的所有其他成员可见,也就是接口中所有成员都可以使用类型变量
  3. 使用泛型接口时,需要显示指定具体的类型,(比如,此处的idFun)
  4. 此时,id方法的参数和返回值类型都是number,ids方法的返回值类型是number[]

3.8.5 泛型类

创建泛型类:

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

}

//指定泛型类的返回值类型
const myNum = new GenericNumber<number>()
myNum.defaultValue = 10

解释:

  1. 在class名称后面添加<类型变量>,这个类就编程了泛型类
  2. 此处的add方法,采用的是箭头形式的类型书写方法
class GenericNumber<NumType>{
  defaultValue: NumType
  add: (x: NumType, y: NumType) => NumType

  constructor(value:NumType){
    this.defaultValue=value
  }
}

//可以省略<类型>,因为类型推断可以自己推断出来
const myNum = new GenericNumber()
myNum.defaultValue = 10

3.9 泛型工具类

Partial

类型可选

interface Props {
  id: string
  children: number[]
}

type PartialProps = Partial<Props>

let p1: Props = {
  id: '',
  children: [1]
}

let p2: PartialProps = {
  id: '',
  children: [1, 3]
}

ReadOnly

只读类型

interface Props {
  id: string,
  children: number[]
}

type ReadonlyProps = Readonly<Props>

let props: ReadonlyProps = {
  id: "1",
  children: [1, 2, 4]
}

props.id = "2"   //这里会报错,只读属性不能赋值

Pick

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

interface Props {
  id: string
  title: string
  children: number[]
}

type PickProps = Pick<Props, 'id' | 'title'>

解释:

  1. Pick工具类型有两个变量:1.选择谁的属性 2.选择哪几个属性
  2. 第二个类型变量传入的属性,只能是第一个变量中存在的属性
  3. 头灶出来的PickProps,只有id和title两个属性类型

Record

泛型工具类 Record<Keys,Type>构造一个独享类型,属性键为Keys,属性类型为Type

// 1.使用Record工具
// type RecordObj = Record<'a' | 'b' | 'c', string[]>

// 2.使用传统方法
type RecordObj = {
  a: string[]
  b: string[]
  c: string[]
}

let obj: RecordObj = {
  a: ['a'],
  b: ['b'],
  c: ['c']
}

解释: Record工具类型有两个类型变量;1.对象有哪些属性 2.对象属性的类型 构建的新对象类型

3.10 签名类型

绝大多数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型 使用场景:当无法确定对象中有哪些属性(或对象中可以出现任意多个属性),这时就用到了索引签名类型了

interface AnyObject {
  [key: string]: number
}

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

解释:

  1. 使用[key:string]来约束该接口中允许出现的属性名称。表示只要是string类型的属性名称,都可以出现在对象中。
  2. 对象obj中就可以出现任意多个属性(比如,a,b等)。
  3. key只是一个占位符,可以换成任意合法的变量名称

3.11 映射类型

映射类型1

基于旧类型创建新类型,减少重复,提升开发效率 比如:类型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 }

//映射类型
type Type2 = { [Key in PropKeys]: number }

//映射类型只能在类型别名中使用,不能在接口中使用
interface Type3 {
  [Key in PropKeys]: number
}

注意:

  1. 映射类型是基于索引签名类型,该类型类似于索引签名类型,也使用了[]
  2. Key in PropKeys表示key可以使PropKeys联合类型中的任意一个,类似于for in /(let key in)
  3. 注意:映射类型只能在类型别名中使用,不能在接口中使用

映射类型2

根据一个对象类型,创建另一个对象类型

type type1 = { a: number, b: number, c: number }
type type2 = { [key in keyof type1]: number }

let obj: type2 = {
  a: 100,
  b: 200,
  c: 300
}

注意:

  1. keyof type1获取到type1中所有键的联合类型,即 'a'|'b'|'c'
  2. Key in...就表示Key可以使type1中所有的键名称中的任意一个

Partial的实现

泛型工具类型(比如:Partial)都是基于映射类型实现的


type MyPartial<T> = {
  [P in keyof T]?: T[P]
}

type Props = { a: number, b: string, c: boolean }
type Props1 = MyPartial<Props>

let obj: Props1 = {
  a: 100,
  b: "你好"
}

解释:

  1. keyof Props表示获取Props的所有键,也就是'a'|'b'|'c'
  2. 在[]后面添加?(问号),表示讲这些属性变为可选的,以此来实现Partial的功能
  3. 冒号后面的T[P]表示获取T中的每个键对应的类型,比如,如果'a'则类型是number;如果'b'则类型是string
  4. 最终type1和props类型完全相同,只是让所有类型变为可选了

索引类型

同时查询索引的多个类型

type Props = { a: number, b: string, c: boolean }

//TypeA和TypeB是一样的
type TypeA = Props['a' | 'b'|'c']
type TypeB = Props[keyof Props]

4.类型声明文件

创建.d.ts文件 类型也可以暴露和引入

// type.d.ts文件
type Props = {
  a: number, b: string
}

export { Props }


//index.ts文件
import { Props } from "./type";

let obj: Props = {
  a: 100,
  b: "hello world"
}

5.react项目

React脚手架工具:create-react-app(简称:CRA),默认支持TypeScript

创建TS的项目命令:npx create-react-app 项目名称 --template typescript`

react-app-env.d.ts文件

react-app-env.d.ts:React项目默认的类型声明文件

/// <reference types="react-scripts" />

解释:告诉TS帮我加载react-script这个包提供的类型声明 react-script类型声明文件包含两部分类型

  1. react、react-dom、node的类型
  2. 图片、样式等模块的类型,以允许在代码中导入图片、svg等文件

TS会自动加载.d.ts文件,以提供类型声明(通过修改tsconfig.json中的include配置来验证)

TS配置文件tsconfig.json

作用:指定项目文件和项目编译所需的配置项 tsconfig.json文件生成命令:tsc --init

  1. 直接编译文件,将忽略tsconfig.json文件:tsc hello.ts --target es6,
  2. 使用tsconfig.json编译文件:tsc
{
  //编译选项
  "compilerOptions": {
    //生成代码的语言版本
    "target": "es5",
    //指定药包含在编译中的library
    "lib": ["dom", "dom.iterable", "esnext"],
    //允许ts编译器编译js文件
    "allowJs": true,
    //跳过声明文件的类型检查
    "skipLibCheck": true,
    //es模块互操作,定比TSModule和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"]
}

/src/App.vue

import { FC } from "react";

type Props = { name: string; age?: number }
  
//创建Hello组件
const Hello: FC<Props> = ({ name, age }) => (
  <div>
    你好,我叫:{name},我今年{age}岁
  </div>
  )
  
//设置默认值
Hello.defaultProps = {
  age: 18
}
  
//创建App组件
const App = () => (
<div>
  <Hello name="张三" age={11}></Hello>
</div>
)

export default App;

只使用TS

import { FC } from "react";


type Props = { name: string; age?: number }


//完全按照函数在TS中的写法
const Hello = ({ name, age=18 }: Props) => (
  <div>
    你好,我叫:{name},我今年{age}岁
  </div>
)

const App = () => (
  <div>
    <Hello name="张三" age={11}></Hello>
  </div>
)

export default App;

事件类型

/src/App.vue

import React, { FC } from "react";
type Props = { name: string; age?: number }

  //创建Hello组件
const Hello = ({ name, age }: Props) => {
  //点击事件
  const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("赞!", e.currentTarget)
  }

  //input的change事件
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value)
  }

  return (
    <div>
      你好,我叫:{name},我{age}岁了

      <button onClick={onClick}>点赞</button>
      <input onChange={onChange} />
    </div>
  )
}

const App = () => (
  <div>
    <Hello name="张三" age={11}></Hello>
  </div>


)

export default App;