# ts 语法进阶

# 初始化一个 ts 开发环境

npm init -y
tsc --init
npm install ts-node -D
npm install typescript --save

然后配置 package.json

"dev": "ts-node ./demo.ts"

# 联合类型和类型保护

联合类型就是一个变量存在多个类型。 解决联合类型中调用属性或方法报错就是类型保护。

interface Bird {
  fly: boolean
  sing: () => {}
}
interface Dog {
  fly: boolean
  bark: () => {}
}

// 联合类型
function trainAnial(animal: Bird | Dog) {
  // 类型保护
  if (animal.fly) {
    // 使用类型断言
    ;(animal as Bird).sing()
  }
  ;(animal as Dog).bark()
  // 公用的属性或者方法会有提示
  animal.fly
}

// 使用 in 语法来做类型保护
function trainAnialSecond(animal: Bird | Dog) {
  // 另一种类型保护
  // 如果有 sing 方法
  if ('sing' in animal) {
    animal.sing()
  } else {
    animal.bark()
  }
}

// typeof 语法来做类型保护
function add(first: string | number, second: string | number) {
  if (typeof first === 'string' || typeof second === 'string') {
    return `${first}${second}`
  }
  return first + second
}

// 使用 instanceof 语法做类型保护
class NumberObj {
  count: number
}
function addSecond(first: object | NumberObj, second: object | NumberObj) {
  if (first instanceof NumberObj && second instanceof NumberObj) {
    return first.count + second.count
  }
  return 0
}

# 枚举类型

js 的例子:

const Status = {
  OFFLINE: 0,
  ONLINE: 1,
  DELETED: 2,
}
function getResult(status) {
  if (status === Status.OFFLINE) {
    return 'offline'
  } else if (status === Status.ONLINE) {
    return 'online'
  } else if (status === Status.DELETED) {
    return 'deleted'
  }
  return 'error'
}
const result = getResult(Status.OFFLINE)
console.log(result)

ts 实现:

// ts 写法 -使用功能枚举类型
enum Status {
  OFFLINE,
  ONLINE,
  DELETED,
}

console.log(Status.OFFLINE) // 0
console.log(Status.ONLINE) // 1
console.log(Status.DELETED) // 2
// 可以进行反查
console.log(Status[0]) // OFFLINE

// 如果想要上面的值都加 1 只需要对枚举类型的 OFFLINE 设置为 1:
// enum Status {
//   OFFLINE = 1,
//   ONLINE,
//   DELETED
// }
// 如果直接设置中间的值 ONLINE 为 4,那么这几个值分别为 0 4 5;

function getResult(status) {
  if (status === Status.OFFLINE) {
    return 'offline'
  } else if (status === Status.ONLINE) {
    return 'online'
  } else if (status === Status.DELETED) {
    return 'deleted'
  }
  return 'error'
}
const result = getResult(Status.OFFLINE)
const result1 = getResult(0)
console.log(result)
console.log(result1)

// 字符串枚举
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}
const value = 'UP'
if (value === Direction.Up) {
  console.log('go up')
}
// 常量枚举 -- 提升性能
const enum Direction1 {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

枚举实现反查的代码:

var Dirsction
;(function (Dirsction) {
  Dirsction[(Dirsction['Up'] = 0)] = 'Up'
  Dirsction[(Dirsction['Down'] = 1)] = 'Down'
  Dirsction[(Dirsction['Left'] = 2)] = 'Left'
  Dirsction[(Dirsction['Right'] = 3)] = 'Right'
})(Dirsction || (Dirsction = {}))
console.log(Dirsction.Down)
console.log(Dirsction[0])

# 泛型

就是泛指的类型,也就是说,定义的时候不确定是什么类型,等到调用的时候采取指定类型:

// 泛型 generic 泛指的类型
function join<T>(first: T, second: T) {
  return `${first}${second}`
}

function anoghterJoin<T>(first: T, second: T): T {
  return first
}

function map<T>(params: Array<T>) {
  return params
}

// 如果前面传 number 后面必须要 number
join<string>('1', '1')
map<string>(['123'])

// 泛型还可以定义多个类型
function add<T, U>(first: T, second: U) {
  return `${first}${second}`
}

add<string, number>('1', 1)
add(1, 1) // 不写,会进行默认的推断

类中的泛型以及泛型类型:

// 在类中使用泛型
class DataManager<T> {
  constructor(private data: T[]) {}
  getItem(index: number): T {
    return this.data[index]
  }
}

const data = new DataManager<string>(['1'])
data.getItem(0)

泛型还可以继承类型

interface Item {
  name: string
}
// 泛型继承 Item ,也就是说 T 里面必须包含 Item 里面所有的东西
class DataManager<T extends Item> {
  constructor(private data: T[]) {}
  getItem(index: number): string {
    return this.data[index].name
  }
}
const data = new DataManager([
  {
    name: 'jiegiser',
  },
])
data.getItem(0)

泛型可以类似实现联合类型:

// string 或者 number
class DataManager<T extends number | string> {
  constructor(private data: T[]) {}
  getItem(index: number): T {
    return this.data[index]
  }
}

interface Test {
  name: string
}
const data = new DataManager<number>([])

泛型还可以声明类型:

function hello<T>(param: T) {
  return param
}
const func: <T>(param: T) => T = hello

约束泛型:例如只传入有 length 属性的变量;也就是通过 extends 关键字实现

interface Iwithlength {
  length: number
}

function echoWithLength<T extends Iwithlength>(args: T): T {
  console.log(args.length)
  return args
}

定义接口使用泛型

interface KeyPair<T, U> {
  key: T
  value: U
}

let kp1: KeyPair<number, string> = { key: 123, value: 'd' }

# 定义全局变量

如果我们使用第三方的库,不是通过 npm 包管理,比如 jquery,可以直接新建一个 jQuery.d.ts 文件,然后定义:

declar var jQuery:(selector: string) => any

这样我们直接在代码写 jQuery 就不会报错,如果 ts 检测不到,可以在 tsconfig.json 中,配置:

{
  "include": ["**/*"]
}

上面的配置就是告诉编译器,编译所有 ts 文件。或则安装 @types/jquery 模块就可以。

# 命名空间 namespace

# 基本用法

类似模块化开发的方式,不必要将所有的变量暴露出去;如果想要将一个类暴露出去使用,可以使用 export 语法来实现:

// 将四个变量变成一个变量
namespace Home {
  class Header {
    constructor() {
      const elem = document.createElement('div')
      elem.innerHTML = 'This is Header'
      document.body.appendChild(elem)
    }
  }

  class Content {
    constructor() {
      const elem = document.createElement('div')
      elem.innerHTML = 'This is Content'
      document.body.appendChild(elem)
    }
  }

  class Footer {
    constructor() {
      const elem = document.createElement('div')
      elem.innerHTML = 'This is Footer'
      document.body.appendChild(elem)
    }
  }

  // 编译完之后 page header content footer 都变成了全局变量,我们真正需要的全局变量是 page

  // 使用命名空间的方式将 page 暴露出去
  export class Page {
    constructor() {
      new Header()
      new Content()
      new Footer()
    }
  }
}

HTML 中调用:

这样就可以使用暴露出的 Page 类;

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./dist/page.js"></script>
  </head>

  <body>
    <script>
      new Home.Page()
    </script>
  </body>
</html>

# 多模块打包以及子命名空间等

将上面的代码我们可以拆分成两个文件 page.ts 跟 components.ts 文件,这两个文件分别定义了两个命名空间;这样方便代码查看,拆分代码后,我们可以这样使用 commponent.ts 中的类 Components.Header() ;可以发现打包结果会是两个文件,我们可以修改 tsconfig.json 的 "outFile": "./build/page.js";将所有的模块打包成一个文件中;

这样打包的结果还是不能使用,需要我们声明互相引用的声明;表示 page.ts 文件依赖这个文件。

///<reference path='./components.ts'>
// page.ts
// 下面的写法是表示这个命名空间引用了 components 的命名空间
///<reference path='./components.ts'>
// 将四个变量变成一个变量
namespace Home {
  // 编译完之后 page header content footer 都变成了全局变量,我们真正需要的全局变量是 page

  // 使用命名空间的方式将 page 暴露出去
  export class Page {
    user: Components.User = {
      name: 'jiegiser',
    }
    constructor() {
      new Components.Header()
      new Components.Content()
      new Components.Footer()
    }
  }
}

// component.ts
namespace Components {
  // 导出子命名空间
  export namespace SubComponents {
    export class Test {}
  }
  // 导出接口这种格式
  export interface User {
    name: string
  }
  export class Header {
    constructor() {
      const elem = document.createElement('div')
      elem.innerHTML = 'This is Header'
      document.body.appendChild(elem)
    }
  }

  export class Content {
    constructor() {
      const elem = document.createElement('div')
      elem.innerHTML = 'This is Content'
      document.body.appendChild(elem)
    }
  }

  export class Footer {
    constructor() {
      const elem = document.createElement('div')
      elem.innerHTML = 'This is Footer'
      document.body.appendChild(elem)
    }
  }
}

多模块打包的时候需要修改 tsconfig.json 里面的配置:

    "module": "amd",
    "outFile": "./build/page.js", // 将所有的模块打包成一个文件
    "outDir": "./dist",
    "rootDir": "./src",

我们还可以在 page.ts 文件中使用 commponent.ts 中声明的接口类型:

// component.ts
namespace Components {
  // 导出接口这种格式
  export interface User {
    name: string
  }
}
// 在 page.ts 中使用
///<reference path='./components.ts'>
namespace Home {
  // 使用命名空间的方式将 page 暴露出去
  export class Page {
    user: Components.User = {
      name: 'jiegiser',
    }
  }
}

在命名空间中也可以导出子命名空间:

// component.ts
namespace Components {
  // 导出子命名空间
  export namespace SubComponents {
    export class Test {}
  }
}

// 使用
Components.SubComponents.Test

# import 对应的模块化

上面的代码中不太容易看到不同的命名空间之间的引入,可以使用 import 做代码的模块化组织; 修改上面的 page.ts 以及 components.ts 文件:

// page.ts
import { Header, Content, Footer } from './components'
export default class Page {
  constructor() {
    new Header()
    new Content()
    new Footer()
  }
}
// components.ts
export class Header {
  constructor() {
    const elem = document.createElement('div')
    elem.innerHTML = 'This is Header'
    document.body.appendChild(elem)
  }
}

export class Content {
  constructor() {
    const elem = document.createElement('div')
    elem.innerHTML = 'This is Content'
    document.body.appendChild(elem)
  }
}

export class Footer {
  constructor() {
    const elem = document.createElement('div')
    elem.innerHTML = 'This is Footer'
    document.body.appendChild(elem)
  }
}

然后需要修改页面中引用的方式,需要采用 amd 的方式进行引用,浏览器不能识别这种 require 语法,所以需要引入 require.js ,如果在 webpack 中使用 ts,他会做很多编译以及加载,不需要引入 require.js 等。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script>
    <script src="./build/page.js"></script>
  </head>

  <body>
    <script>
      require(['page'], function (page) {
        new page.default()
      })
    </script>
  </body>
</html>

# 使用 Parcel 打包 TS 代码

首先进行安装 parcel :npm install parcel@next -D,然后修改 package.json 配置:

  "scripts": {
    "test": "parcel ./src/index.html"
  },

这个打包工具会分析我们打包的 index.html 文件,我们打包的文件引入了 ts 文件,他会进行自动编译,然后进行引入;

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./page.ts"></script>
  </head>

  <body></body>
</html>

# 描述文件中的全局类型 (d.ts)

比如我们在页面中引用了 cdn 的 jquery 工具,但是在编译器中我们没有进行安装 jquery 包,这样会导致在编写 jquery 相关的代码时候编辑器会提示报错。我们可以创建一个 jquery.d.ts 的 jquery 描述文件里面去定义 jquery 相关的方法,这个编辑器就不会提示找不到 $ 等的变量以及方法:

jquery.d.ts

// 声明全局变量 $,参数为一个函数,返回值为空
declare var $: (param: () => void) => void

interface jQueryInstance {
  html: (html: string) => jQueryInstance
}
// 定义全局函数
// 函数重载
declare function $(readyFunc: () => void): void
declare function $(params: string): jQueryInstance

// 可以使用 interface 的语法,实现上面一样的重载,表示 $ 可以通过两种方法实现函数。
interface JQuery {
  (readyFunc: () => void): void
  (params: string): jQueryInstance
}

declare var $: JQuery

// 声明一个对象
// 定义对象 new $.fn.init(), 以及对类进行定义,以及命名空间的嵌套
declare namespace $ {
  namespace fn {
    // init 是一个 class
    class init {}
    // init 是一个 function
    function init(): void
  }
}

# 模块代码的类型描述文件

比如下面对 jquery 库进行类型描述文件的定义: 下面是 ts 代码

// 如果没有类型文件定义页面会报错
import $ from 'jquery'
$(function () {
  $('body').html('<div>123</div>')
  new $.fn.init()
})

我们定义一个 jquery.d.ts 的类型描述文件:

// ES6 模块化
declare module 'jquery' {
  interface jQueryInstance {
    html: (html: string) => jQueryInstance
  }
  // 定义全局函数
  // 函数重载
  function $(readyFunc: () => void): void
  function $(params: string): jQueryInstance
  // 定义对象 $.fn.init(), 以及对类进行定义,以及命名空间的嵌套
  namespace $ {
    namespace fn {
      class init {}
    }
  }
  // 关键需要导出
  export = $
}

# 泛型中 keyof 语法的使用

keyof 相当于对 Person 中的属性进行循环;

interface Person {
  name: string
  age: number
  gender: string
}

class Teacher {
  constructor(private info: Person) {}
  getInfo(key: string): {
    if (key === 'name' || key === 'age' || key === 'gender') {
      return this.info[key]
    }
  }
}
const teacher = new Teacher({
  name: 'jiegiser',
  age: 18,
  gender: 'male',
})
// 如果传入 Person 中没有的属性,就会报错,如果通过类型保护,如上面代码,返回的结果就是 string | number | undefined,类型不能明确
console.log(teacher.getInfo('name'))


// 可以使用功能 keyof 以及 泛型解决上面的问题

// keyof 相当于对 Person 中的属性进行循环
// 第一次循环
// type T = 'name'
// key: 'name'
// Person[key]: string
// // 第二次循环
// type T = 'age'
// key: 'age'
// Person[key]: number
// // 第三次
// type T = 'gender'
// key: 'gender'
// Person[key]: string

// 原理就是使用了 type
type NAME = 'name'
// 会报错
// const names: NAME = '1231'
const names: NAME = 'name'

class Teacher {
  constructor(private info: Person) {}
  getInfo<T extends keyof Person>(key: T): Person[T] {
    return this.info[key]
  }
}

# 进阶

  • 官方文档
  • https://ts-ast-viewer.com/# 根据 ts 代码生成抽象语法树
  • 在第三方库的官网搜索与 ts 的结合使用

评 论:

更新: 11/21/2020, 7:00:56 PM