深入浅出-Typescript

178 阅读17分钟

1.Typescript介绍

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

JS的类型系统本身就存在“不足”,JS代码中绝大部分错误都是类型错误(Uncaught TypeError)。 从编程语言的动静来区分,TypeScript属于静态类型的编程语言,JS属于动态类型的编程语言。 静态类型:编译期做类型检查;动态类型:执行期做类型检查。 代码编译和代码执行的顺序:1 编译 2执行。

1.2 Typescript相比JS的优势?

1.更早(写代码的同时)发现错误,减少找bug、改bug时间,提升开发效率。 2.程序中任何位置的代码都有代码提示,随时随地的安全感,增强了开发体验。 3.强大的类型系统提升了代码的可维护性,使得重构代码更加容易。 4.支持最新的ECMAScript语法,优先体验最新的语法,让你走在前端技术的最前沿。 5.TS类型推断机制,不需要在代码中每个地方显示标注类型,让你在享受优势的同时,尽量降低了成本。

2.Typescript初体验

2.1 安装编译TS的工具包

用来编译TS代码的包,提供了tsc的命令,实现了TS->JS的转化。 验证是否安装成功:tsc -v(查看typescript的版本)。

npm install -g typescript

image.png

2.2 编译并运行TS代码

  1. 创建hello.ts文件(注意:TS文件的后缀名为.ts)
  2. 将TS编译为JS: 在终端中输入,tsc hello.ts(此时,在同级目录中会出现一个同名的JS文件)。
  3. 执行JS代码:在终端中输入命令,node hello.js

image.png

2.3 简化运行TS代码

问题描述:每次修改代码后,都要执行两个命令,才能运行TS代码,非常繁琐。 简化方式:

npm i -g ts-node

使用方式: ts-node hello.ts
解释:ts-node命令其实内部还是偷偷的将TS->JS,然后,再运行JS代码。

3.Typescript常用类型

3.1 常用基础类型概述

可以将TS中的常用基础类型细分为两类,1.JS已有类型。2.TS新增类型。
1.JS已有类型:number,string,boolean,null,undefined,symbol,object
2.TS新增类型:联合类型,自定义类型(类型别名),接口,元祖,字面量类型,枚举,void,any等。

3.2 联合类型

let arr: (number | string)[] = [1, 'a', 3, 'b']

解释:|(竖线)在TS叫做联合类型(由2个或多个其他类型组成的类型,表示可以是这些类型中的任意一种)。

3.3 类型别名

type CustomArray = (number | string)[];
let arr1: CustomArray = [1, 'a', 3, 'b'];

3.4 接口

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

  1. 使用interface关键字来声明接口。
  2. 接口名称(比如:此处IPerson),可以是任意合法的变量名称。
  3. 声明接口后,直接使用接口名称作为变量的类型。
interfance IPerson {
   name: string;
   age: string;
   sayHi: () => void;
}
let person: IPerson = {
   name: 'jack',
   age: 19,
   sayHi: () => {
     console.log('say')
   }
}

3.5 接口和类型别名区别

相同点:都可以给对象指定类型。 不同点: 接口,只能为对象制定类型。 类型别名,不仅可以为对象制定类型,实际上可以为任意类型指定别名。

interface IPerson {
  name: string;
  age: number;
  sayHi(): void;
}

type IPerson = {
  name: string;
  age: number;
  sayHi(): void;
}
type NumStr = number | string;

3.6 元组

元组类型是另一种类型的数组,它确切地知道包含了多少个元素,以及特定索引对应的类型。

let position: [number, number] = [39.5427, 116.2317]

3.7 类型断言

有时候你会比TS更加明确一个值的类型,此时,可以使用类型断言来指定更具体的类型。 比如:

const alink = document.getElementById('link') as HTMLAnchorELement

HTML类型如何确定: 就是当你想看哪个HTML的时候,就会有$0

image.png 然后

console.dir($0)

image.png

3.8 字面量类型

let str1 = 'Hello TS'
const str2 = 'Hello TS'

通过TS类型推论机制,可以得到答案:

  1. 变量str1的类型为:string
  2. 变量str2的类型为:'Hello TS' 解释:
  3. str1是一个变量(let),它的值可以是任意字符串,所以类型为:string。
  4. str2是一个常量(const),它的值不能变化只是‘Hello TS’,它的类型为:‘Hello TS’ 除了字符串外,任意的JS字面量(比如对象、数字等)都可以作为类型使用。
function changeDirection(direction: 'up' | 'down' | 'left'|'right') {
  console.log(direction)
}

3.9 枚举

enum Direction { Up, Down, Left, Right }
function changeDirection(direction: Direction) {
  console.log(direction)
}
changeDirection(Direction.Up)

enum Direction {
Up = 2,
Down = 4,
Left = 8,
Right = 16
}
changeDirection(Direction.Up)

// 字符串枚举
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT'
}
console.log(Direction['Up']) // 'UP'
注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值。

// 反向映射
enum role {
  student,
  teacher,
  admin
}
console.log(role.admin) // 2
console.log(role['teacher']) //1
console.log(role[0]) // 'student'

3.10 typeof

let p = {x: 1, y: 2}
function formatPoint(point: typeof p) {
   
}
// typeof p 就会转换为{x: number, y: number}

3.11 unknown

unknown和any的区别:unknow具备any的功能同时也保留着静态检查的能力,any就不会静态检查了。

const test: unknown = 'string';
test.substr(1) // 这个时候就会报类型错误
const test2: any = 1000
1000.substr(1) // 不会报错

3.12 void

void和undefined的区别就在于undefined是void的一个子集。void不关注具体返回什么值,都可以。

3.13 never

never是指没法正常结束返回的类型,一般是用在报错或者死循环的函数里。

function test1(): never { throw new Error('error')}
function test2(): never {while(true) {}}
type Test3 = 'n' | never // 返回'n'
function test4 {
   test1()
   console.log('永远都不会执行到这条语句,因为上面这个函数返回了never')
}

4. Typescript 高级类型

4.1 TS中的高级类型有很多

1.重点学习以下高级类型:
class类
类型兼容性
交叉类型
泛型和keyof
索引签名类型和索引查询类型
映射类型

4.2 class类

class Person {
  age: number
  gender: string
  constructor(age: number, gender: string) {
    this.age = age;
    this.gender = gender
  }
  scale(n: number): void {
     this.x* = n;
     this.y* = n;
  }
}
const p = new Person(18, '男')

4.3 class类继承

1.extends(继承父类)2.implements(实现接口) 说明JS中只有extends,而implements是TS提供的。

class Animal {
   move() {
     console.log('Moving along')
   }
}
class Dog extends Animal {
   bark() {
      console.log('汪')
   }
}
const dog = new Dog()

interface Singale {
  sing(): void;
}

class Person implements Singale {
  sing() {
    console.log('object')
  }
}

4.4 class类的protected

protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见。

class Animal {
  protected move() {
    console.log('Moving along')
  }
}
class Dog extends Animal {
  bark() {
    console.log('汪')
    this.move()
  }
}

4.5 class类的private

private: 表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的。

class Animal {
  private move() {
    console.log('moving')
  }
  walk() {
   this.move()
  }
}

4.6 class类的readonly

readonly: 表示只读,用来防止在构造函数之外对属性进行赋值。

class Person {
   readonly age: number = 18
   constructor(age: number) {
     this.age = age
   }
}

使用readonly关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法。 注意:属性age后面的类型注解(比如,此处的number)如果不加,则age的类型为18(字面量类型)。 接口或者{}表示的对象类型,也可以使用readonly

interface NameArr {
  readonly name: string;
}
const abc = (test: NameArr) => {
  
}

let obj: {readonly name: string} = {
  name: 'jack'
}
obj.name = 'rose' // 报错

4.7 交叉类型

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

interface Person {
  name: string;
}
interface Contact {
  phone: string;
}

type PersonDetail = Person & Contact
let obj: PersonDetail = {
  name: 'jack',
  phone: '1234343434'
}

4.8 交叉类型(&)和接口继承(extends)的对比:

  • 相同点:都可以实现对象类型的组合。
  • 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。
interface A {
  fn: (value: number) => string;
}
interface B extends A {
  fn: (value: string) => string;
}
// 以上代码,接口继承会报错。

type C = A & B
C = {
  fn: ( Value: string | number) => string
}
交叉类型不会报错

4.9 泛型:

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

function id<Type>(value: Type): Type {
  return value;
}
id<number>(10); // 类型number
id<string>('10'); // 类型string

4.10 简化泛型函数调用

function id<Type>(value: Type): Type {
  return value;
}
let num = id(10)

解释:

  1. 在调用泛型函数时,可以省略<类型>来简化泛型函数的调用。
  2. 此时,TS内部会采用一种叫类型参数推断的机制,来根据传人的实参自动推断出类型变量Type的类型。
  3. 比如,传入实参10,TS会自动推断出变量num的类型number,并作为Type的类型。

4.11 泛型约束

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

指定更加具体的类型,如下:

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

添加约束,如下:

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属性。 所以在这里extends不是继承的意思了,表示传入的类型变量需要满足ILength Type必须是Ilength的子集。

4.11.2 泛型条件

这里就不限制T一定要是U的子类型,如果是U子类型,则将T定义为X类型,否则定义为Y类型

T extends U ? X : Y

还有一种情况,如果把X换成T,如此形式:T extends U ? T:never 此时返回的T,是满足原来的T中包含U的部分,可以理解为T和U的交集

type Test<T> = T extends {t: infer B} ? B: string

Test<{a: number, t: number}> // 返回 number

4.12 泛型变量多个情况

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

function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
   return obj[key]
}

let person = {name: 'jack', age: 18}
getProp(person, 'name')

解释:
1.keyof关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。
2.本例子中keyof Type实际上获取的是person对象所有键的联合类型,也就是: 'name' | 'age'。
3.类型变Key受Type约束,可以理解为:Key只能是Type所有键中的任意一个,或者说只能访问对象中存在的属性。

4.13 泛型接口

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. 使用泛型接口时,需要显示指定具体的类型(比如,此处的IdFunc)。
  4. 此时,id方法的参数和返回值类型都是number;ids方法的返回值类型是number[]

4.14 泛型类

class GenericNumber<NumType> {
  defaultValue: NumType
  add: (x: NumType, y: NumType) => NumType
}
const myNum = new GenericNumber<number>();
myNum.defaultValue = 10;    

4.14 泛型工具类型--Partial

将type的所有属性设置为可选。

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

4.15 泛型工具类型-Readonly

将type的所有属性设置为只读。

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

4.16 泛型工具类型-Pick

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

4.17 泛型工具类型-Record

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

4.18 索引签名类型

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

interface AnyObject {
  [key: string]: number
}

解释:
1.使用key来约束该接口中允许出现的属性名称。表示只要是string类型的属性名称,都可以出现在对象中。
2.这样,对象obj中就可以出现任意多个属性(比如,a,b等)。
3.key只是一个占位符,可以换成任意合法的变量名称。
4.隐藏的前置知识:JS中的对象({})的键是string类型的。
在js中数组是一类特殊的对象,特殊在数组的键(索引)是数值类型。
并且,数组也可以出现任意多个元素。所以,在数组对应的泛型接口中,也用到了索引签名类型。

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

4.19 映射类型

映射类型:基于旧类型创建新类型(对象类型),减少重复,提升开发效率。

// bad
type PropKeys = 'x' | 'y' | 'z';
type Type1 = {x: number; y: number; z: number};

// good
type PropKeys = 'x' | 'y' | 'z';
type Type2 = { [Key in PropKeys]: number };

// 报错
interface Type3 {
  [Key in PropKeys]: number
}

注意:映射类型只能在类型别名中使用,不能在借口中使用。

映射类型除了根据联合类型创建新类型之外,还可以根据对象类型来创建:

type Props = {
  a: number;
  b: string;
  c: boolean;
}
type Types = {
 [key in keyof Props]: number
}

解释:

  1. 首先,先执行keyof Props获取到对象类型Props中所有键的联合类型即:'a'|'b'|'c'。
  2. 然后,key in ...表示Key可以是Props中所有的键名称中的任意一个。

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

// 冒号后面的T[P]表示获取T中每个键对应的类型。
type Partial<T> = {
  [P in keyof T]?: T[P]
}

type Props = {
 a: number;
 b: string;
 c: boolean;
}
type PartialProps = Partial<Props>

刚刚用到的T[P]语法,在TS中叫做索引查询(访问)类型。 作用:用来查询属性的类型。

type Props = {
  a: number;
  b: string;
  c: boolean;
}
type TypeA = Props['a']; //number类型

索引查询类型的其他使用方式:同时查询多个索引的类型

type Props = {
 a: number;
 b: string;
 c: boolean;
}
type TypeA = Props['a' | 'b'] // string | number
type TypeB = Props[keyof Props] // string | number | boolean

5.类型声明文件

今天几乎所有的js应用都会引入许多第三方库来完成任务需求。 这些第三方库不管是否是用TS编写的,最终都要编译成JS代码,才能发布给开发者使用。 我们知道是TS提供了类型,才有了代码提示和类型保护等机制。 但在项目开发中使用第三方库时,你会发现它们几乎都有相应的TS类型,这些类型是怎么来的呢?类型声明文件。 类型声明文件:用来为已存在的JS库提供类型信息。 这样在TS项目中使用这些库时,就像用TS一样,都会有代码提示,类型保护等机制了。

5.1 第三方库的类型声明文件

第三方库的类型声明文件有2种存在形式:1.库自带类型声明文件。2.由DefinitelyType提供。
1.库自带的类型声明文件: 比如,axios。 这种情况下,正常导入该库,TS就会自动加载库自己的类型声明文件,以提供该库的类型声明。
2.由DefinitelyType提供 基本上都是以@types/*提供类型声明包。
举个例子,使用lodash时,发现没有自带的类型声明文件,有2种方案可以解决。

// 第一种方案就是安装@type/lodash,你一旦安装完后,TS就会自动加载该类声明包,以提供该库的类型声明。
pnpm install @type/lodash -D
// 第二种方案
直接在项目中创建一个类型声明文件,通过使用declare module 'lodash'即可。

5.2 自己创建一个类型声明文件

创建自己的类型声明文件:1. 项目内共享类型 2.为已有JS文件提供类型声明。
1.项目内共享类型:如果多个.ts文件中都用到同一个类型,此时可以创建.d.ts文件提供该类型,实现类型共享。

操作步骤:

  1. 创建index.d.ts类型声明文件。
  2. 创建需要共享的类型,并使用export导出。
  3. 在需要使用共享类型的.ts文件中,痛过import导入即可。
// index.d.ts
export type Type1 = {
  a: string;
}

// 定义一个模块,模块里面可以放该模块下的类型
declare module "url" {
  export interface Url {
    protocol?: string;
    hostname?: string;
    pathname?: string;
  }
  export function parse(
    urlStr: string,
    parseQueryString?: string,
    slashesDenoteHost?: string,
  ): Url;
}
// 当然你也可以简写,如果一旦简写了,那么里面所有的导出类型都是any;
declare module "url"

// test.tsx
import Type1 from index;

5.3 为已有js文件提供类型声明。

在将JS项目迁移到TS项目时,为了让已有的.js文件有类型声明。 在导入.js文件时,TS会自动加载与.js同名的.d.ts文件,以提供类型声明。 delcare关键字:用于类型声明,为其他地方(比如,.js文件)已存在的变量声明类型,而不是创建一个新的变量。

  1. 对于type、interface等这些明确就是TS类型的,可以省略delcare关键字
  2. 对于let,function或者是函数表达式等具有双重含义(在JS,TS中都能用),应该使用declare关键字,明确指定此处用于类型声明。
// utils.js
let count = 10;
let position = {
    x: 0,
    y: 0
}
function add (x, y) {
    return x+y;
}

function changeDirection(direction) {
    console.log(direction)
}

const pointInfo = point => {
    console.log('当前坐标', point)
}

export { count, add, changeDirection, pointInfo }
// utils.d.ts,要写在同一个目录下面,且文件名和js一致,则不需要export也可以,如果不在同一个目录下面,需要通过export进行导出才可。react脚手架最外层的声明文件名字是可以改动的,都会读到。
declare let count: number

interface Point {
    x: number;
    y: number;
}

declare let position: Point

declare function add(x: number, y: number): number

type Direction = 'up' | 'down' | 'left' | 'right'
declare function changeDirection(direction: Direction): void;

declare const pointInfo: (point: Point) => void;


export {
    count,
    add,
    changeDirection,
    pointInfo
}

6.React中使用TypeScript

使用CRA创建支持TS的项目 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配置来验证)。

6.1 React中的tsconfig.json

tsconfig.json可以自动生成,命令:tsc --init

{
  "compilerOptions": {
  // 生成js代码的语言版本
    "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,
    // 编译时不生成任何文件(只进行类型检查),使用babel进行处理,相关的loader
    "noEmit": true,
    // 指定jsx编译成什么形式
    "jsx": "react-jsx"
  },
  // 指定允许ts处理的目录
  "include": [
    "src"
  ]
}

6.2 React中的常用类型

react是组件化开发模式,React开发主要任务就是写组件,两种组件:1.函数组件 2.class组件。 1.函数组件,主要包括以下内容:
1.组件类型
2.组件的属性(props)
3.组件属性的默认值(defaultProps)

const Hello:FC<Props> = ({name, age=18}) => {
  return (
  <>
  <div{name}</div>
  <div>{age}</div>
  </>
  )
}

4.事件绑定和事件对象

<button onClick={onClick}></button>
const onClick = () => {}
const onClick1 = (e: React.MouseEvent<HTMLButtonElement>) => {}

再比如,文本框:

<input onChange={onChange}></input>
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {}