Featured image of post TypeScript两天速成

TypeScript两天速成

前言

作为后端开发者,很多时候会面对前端开发的需求。就我最常用的 Golang 而言,它的 template 能够快速且简洁的开发一些简单的前端页面。

但是也有很多时候,我们可能想去实现比较复杂一些的前端页面,更好的展示后端功能。因为对于一些软件的使用者,他们并不会像我们一样,使用 CLI 、API 等接口去使用。一个精美的、一目了然的网页更能给他们使用下去的欲望。

再或者,谁不想向“全栈工程师”更近一步呢?在 CNCF landscape 蓬勃发展的时代,几乎每个项目都会有自己的站点、有 Dashboard。尤其是一些可观测性方面的项目。前端技术让晦涩难懂的代码华丽的展示在人们面前,有时候,仅仅是因为前端做的好看就能够收获一大批用户。在这个人人好像都有全栈开发能力的时代,只会写一些后端代码总感觉差点意思。

同时,博客框架、站点框架等前端框架,使得我们可以通过改改配置文件、修改几行代码就能将自己的内容以网站的形式展现,这是极为方便的。但无论如何,我们都避免不了去定制化的开发前端内容。

Vue和React可谓知名度最大的前端框架之二,其开源生态带来的丰富的教程、多样的组件,很大程度上减少了的前端开发的复杂性。对于很少接触前端以及缺少相关理论知识的开发者来说,上手这两个框架还是需要一定的基础。

我决定在这个寒假正式把前端能力的提升加入日程,目标是在有前端需求时,能够快速的通过 Vue/React 脚手架和开源组件,将前后端分离的技术用于自己的开发中,快速为后端应用搭建一个看的过去的前端页面。

为什么是 Typescript

我常常觉得 Js 代码混乱,没有结构性,不够简洁和优美。可能是因为 Go 写的太多的原因,很少有主流语言像 Go 有这么少的特性、关键字、语法糖了吧,它是静态语言的特点能把尽可能多的问题及时暴露在开发阶段。Typescript 的很大程度上减少了 Javascript 在开发中的不确定性,这种优势能够提高 Debug 的效率和程序运行的确定性。同时,从我的角度,前端开发正在慢慢拥抱 Typescript,它为前端工程带来的优点是有目共睹的。Vue/React 都支持Typescript,相对于 Javascript 来说,可能后端程序员直接以 Typescript 作为自己的第一门 “前端语言” 是不是更好些呢?(我没怎么使用过 Javascript,不敢妄下断论。)

所以,我决定以 CSS布局➡️TS➡️Vue||React 的线路,比较系统把前端开发能力补齐。这个过程肯定有很多东西“不求甚解”,但我想,只要我能够用起来,在前端能够实现自己的想法就够了。

通过本文,希望可以快速覆盖常用的 TS 知识点,快速上手,为使用框架做铺垫。

基本数据类型

需要理解 Typescript 对于 Javascript 的能力、安全性、可预测性、规范性的能力,绝大部分来自编译器给我们的限制。事实上在底层,Typescript 也是翻译成 Javascript 来运行的。其中一些特性,我们需要带入编译器的角度,即:在翻译成 Javascript 做了什么,便会好理解一些。

  • number:可以是整数,可以是浮点数
  • boolean
  • string

const 和 let

  • const:定义的值不可变,对于不变的值,我们尽量多用 const 保证变量不被滥用
  • let:定义的值可变
  • 定义时可以不告知变量,编译器自己推到:
const a: string   ='123'
// const a ='123' ok
let b: string = '23'
// let b = '23' ok

literal 数据类型

let ans: 'yes' | 'no' | 'maybe' = 'yes'
let httpStatus: 200 | 404 | 500 = 200
let httpStatusStr: 200 | 404 | '500' | '502' = '502'

给这个变量限定特定的取值。有点类似枚举。

literal的值可以赋给其他变量,但是类型必须相同。

let s = ans	// let s: "yes" 这样 let 类型
// 或者
let s:string = ans	// ok
let s:string = httpStatusStr // ok httpStatusStr 类型是字符串
// let s:number = httpStatusStr 就不行 因为 httpStatusStr 是字符串


s = 'abcd'
// 但是 ans = s 就不行

let httpStatusStr: 200 | 404 | '500' | '502' = '502'这种类型怎么理解?

Union of Types

let httpStatusStr: 200 | 404 | '500' | '502' = '502'

function f(s: 200 | '502') {
    let status: string | number = s
    // let status: string = s 错误
    // let status: number = s 错误
}

顾名思义,类型的并集。

any

通过any类型,让编译器不进行语法检查。

let a:any = 'abc'
a = 123 // ok
a = {}
a.name = 'jack'

undefined

一旦定义为undefined,值就只能是undefined

let c:undefined = undefined

一般,用在这种情况,实现可选参数:

let httpStatusStr: 200 | 404 | '500' | '502' | undefined = undefined

function f(s: 200 | 404 | '500' | '502' | undefined) {
    
}

枚举

  • 枚举是 Typescript 特有的类型,JS 没有。
enum HTTPStatus {
    OK,
    NOT_FOUND,
    INTERNAL_SERVER_ERROR,
}

function processeHTTPStatus(s: HTTPStatus) {
    console.log(s)
}

processeHTTPStatus(HTTPStatus.NOT_FOUND)
// [LOG]: 1 
  • 支持定义枚举变量
enum HTTPStatus {
    OK = 200,
    NOT_FOUND = 404,
    INTERNAL_SERVER_ERROR = 500,
}
  • 支持和 union of types 混用
enum TimingFunc {
    LINEAR = 'linear',
    EASE = 'ease',
    EASE_IN = 'ease-in',
}

function tf(timingFunc:
    | 'linear' | 'ease' | 'ease-in') {
        console.log(timingFunc)
    }

tf(TimingFunc.EASE)
// "ease" 
  • 从类型值推导回类型,类似 map 结构通过 key 寻找 value
function tfBack(timingFunc: HTTPStatus) {
        console.log(HTTPStatus[timingFunc])
    }

tfBack(HTTPStatus.INTERNAL_SERVER_ERROR)
// "INTERNAL_SERVER_ERROR"

数组类型

  • 使用 const 定义的数组元素是可以修改的
let nums = [1,2,3]
// or
let nums: number[] = [1,2,3]
// 或者使用泛型
let nums: Array<number> = [1,2,3]
  • 数组也支持多种类型
let nums = [1,2,3,'a']
// let nums: (string | number)[]
  • 其他基本操作:遍历,长度,从头/尾增加删除,切片
  • 判断数组是否为空要使用长度来判断
// 遍历

// 长度
nums.length
// 从最右侧追加 类似队列
nums.push(6)
// 去除最右边的值
nums.pop()

// 从最左侧增加元素
nums.unshift(7)
// 相应的 拿掉最左边的元素
nums.shift()

// 切片 像 golang 一样前开后闭
const num = [0,1,2,3,4,5,6,7]
num.slice(2,5)	// [2,3,4]
anum.slice(2)	// [2, 3, 4, 5, 6, 7] 

// 删除元素 返回删除的内容
delete = num.splice(3,2)	// 从下标 3 开始删除 2 个元素
// 从下标 3 开始删除 2 个元素 然后在对应位置填充 9,8,7 
delete = num.splice(3,2,9,8,7)

// 查找元素下标
idx = nums.indexOf(2)		// 元素值所对应的下标
idx = nums.indexOf(25)	   // 元素值 2 所对应的下标 从下标 5 开始寻找
idx = nums.lastIndexOf(2)	// 从后向前找
  • 数组排序的 sort() 函数默认使用 ASCII 码顺序排列,具体在后文 函数式编程 部分中说明。
  • 同时,forEach 遍历数组也在后文章节 函数的参数可以是函数

元组

  • 虽然 Typescript 没有原生的元组语法,但是可以利用数组实现
const a = [1,2,3]
const [a1,a2,a3] = a
console.log(a1,a2,a3)	// 1,  2,  3 

对象

  • 快速创建一个对象
const emp1 = {
    name: {
        first: 'li',
        last: 'duo',
    },
    gender: 'male' as 'male' | 'female' | 'other',
    salary: 9000,
    bonus: undefined as (number | undefined),
    badges: ['a', 'b'],
}

emp1.bonus = 100000
emp1.gender = 'other'

console.log(emp1)
// {
//   "name": {
//     "first": "li",
//     "last": "duo"
//   },
//   "gender": "other",
//   "salary": 9000,
//   "bonus": 100000,
//   "badges": [
//     "a",
//     "b"
//   ]
// } 
  • 对象转 json 字符串 以及 json 字符串转对象
const s: string = JSON.stringify(emp1)
console.log(s)
// "{"name":{"first":"li","last":"duo"},"gender":"other","salary":9000,"bonus":100000,"badges":["a","b"]}" 

// 再由 json 字符串转回对象
// 但是 这种字符串转换的对象 编译器并不能知道它具体是什么类型 会认为 emp2: any
const emp2 = JSON.parse(s)

逻辑控制

if/else

  • 判断相等,一定用===
  • 判断不相等,一定用!==
  • 如果遇到类型组合的参数,就不用担心有意外情况,因为对于类型组合参数来说,非法数值根本无法当作参数,如下面的例子:
function processeHttpStatus(s:200 | 404 | '500' | '502'){
    if (s===200) {
        console.log('ok')
    } else if (s==='500') {
        console.log('500')
    }
    // 不需要 除了 200 | 404 | '500' | '502' 以外的值传不进来
    // else {
    //    console.log('unknown')
    // }
}
processeHttpStatus(200)			// "ok" 

switch

  • 注意,不像 go,需要在每个分支中加 break
function processeHttpStatus(s:200 | 404 | '500' | '502' | undefined){
    const status = typeof s === 'string' ? parseInt(s) : s
    switch (status) {
        case 200:
        console.log('ok')
        break
        case 502:
        console.log('502')
        break
        default:
        console.log('unknown')
        break
    }
}

for 和 while

let sum = 0
for (let i = 0; i < 100; i++) {
    sum += i
}

let i = 1
while (i <= 100) {
    sum += i
    i++
}

try/catch

  • 保护程序在运行中出现错误时还能继续运行下去。
let sum = 0
for (let i = 0; i < 100; i++) {
    try {
        sum += i
        if (i % 17 === 0) {
            throw `bad number ${i}`
        }
    } catch (err) {
        console.error(err)
    }
}

console.log(sum)

// [ERR]: "bad number 0" 
// [ERR]: "bad number 17" 
// [ERR]: "bad number 34" 
// [ERR]: "bad number 51" 
// [ERR]: "bad number 68" 
// [ERR]: "bad number 85" 
// [LOG]: 4950 

函数

  • 函数定义
    • 注意可选参数的使用,可选参数传入就是 undefined
    • 注意默认值的使用
    • 注意可变参数列表的使用
function add(
    a: number, b: number, 
    c?: number, d: number=0, 
    ...e: number[]): number {
    let sum = a + b + (c||0) + d
    for (let i=0; i< e.length; i++) {
        sum += e[i]
    }
    return sum
}
const nums = [1,2,3,4]
add(1,2,3,4,...nums)
// add(1,2,3,4,1,2,3,4) 也行 不能直接传 nums 需要加 ...
  • 对象类型做参数:使用对象类型做参数使得参数易于理解
function send(params: {
    url: string,
    method: 'GET' | 'POST' | 'PUT',
    header: object,
    data?: string,
    ack: number[]
}): boolean {
    return true
}

send({
    url: 'mockurl',
    method: 'GET',
    header: {
        contentType: "",
    },
    ack: [101, 102],
})

为对象定义方法

  • 和在对象外定义的不同,在于不需要写 function 关键字,其他的 参数列表、返回值 的定义都相同。
  • 注意,使用对象的类型需要使用 this. 来告诉编译器使用这个对象中的变量。
const emp1 = {
    name: {
        first: 'li',
        last: 'duo',
    },
    gender: 'male' as 'male' | 'female' | 'other',
    salary: 9000,
    bonus: undefined as (number | undefined),
    badges: ['a', 'b'],
    upDateSalary(a: number): void {
        this.salary += a
    },
}

emp1.upDateSalary(2000)

console.log(emp1.salary)
// 11000 

函数式编程风格

Typescript 中,函数是“一等公民”

与 golang 一样。Typescript 天生支持函数式编程。

  • lambda 表达式,在 JS/TS 中叫 箭头函数:let comp = (a: number, b:number) => a-b
  • 注意使用 箭头函数 和使用 function 的区别

变量的类型可以是函数

let comp = (a: number, b:number) => {
    return a-b
}
// or
let comp = (a: number, b:number) => a-b

等同于:

let comp = function (a: number, b:number): number {
    return a-b
}

值 (literal) 可以是函数

a.sort((a: number, b:number) => a-b)

或者,加大括号就要像函数体中一样,需要明确的 return

a.sort((a: number, b:number) => { 
    ...
    return a-b
})

对象的字段可以是函数

为对象定义方法

函数的参数可以是函数

let a = [3,4,1,2,44,5,78,32,12,1,1,2]

a.sort((a: number, b:number) => {
    return a-b
})

console.log(a)
// [1, 1, 1, 2, 2, 3, 4, 5, 12, 32, 44, 78]

等同于:

function comp(a: number, b:number): number {
    return a-b
}

a.sort(comp)

再或者,使用 forEach遍历数组:

const a = [1,2,3,4,5,6,7,8,9]

const b: number[] = []
a.forEach((v) => {
    b.push(v*v)
})

函数的返回值可以是函数

function createComp() {
    return (a: number, b: number) => a-b
}

a.sort(createComp())

参数和返回值同时是函数,这里有点类似于 Gin 框架的串联中间件,例如在转发请求之前记录日志:

function createComp() {
    return (a: number, b: number) => a-b
}

function logComp(comp: (a: number, b: number) => number) {
    return (a: number, b: number) => {
        console.log('comp',a,b)
        return comp(a, b)
    }
}

a.sort(c(logComp()))

函数的闭包

核心思想是:控制变量的可见范围,避免不期望的操作。

部分应用函数

在参数数量不匹配时,把函数用起来,就使用 函数闭包,实现部分应用函数:

const GoodFactor = 2
const a = [1,2,3,4,5,6,7,8,9]

// 接收两个参数 返回 boolean
function isGoodNumber(goodFactor: number, v: number) {
    return v % goodFactor === 0
}

// 接收两个参数 第二个参数是个函数,该函数只有一个参数
function filterArray(a: number[], f: (v: number) => boolean) {
    return a.filter(f)
}

// 那么如何使用 filterArray,把 isGoodNumber 作为第二个参数传入呢?
// 利用 函数闭包,给构造一个合法的函数
// (v) => isGoodNumber(GoodFactor, v) 等同于 (v) => isGoodNumber(GoodFactor, v) => boolean 也就相当于 (v) => boolean
filterArray(a, (v) => isGoodNumber(GoodFactor, v))

等同于:

function isGoodNumber(goodFactor: number, v: number) {
    return v % goodFactor === 0
}

function filterArray(a: number[], f: (v: number) => boolean) {
    return a.filter(f)
}

const GoodFactor = 2

const a = [1,2,3,4,5,6,7,8,9]

// 利用这个函数 把双参数的 isGoodNumber 封装成单参数
function partialApply(f: (a: number, b: number)=>boolean, gn: number) {
    return (v: number) => {
        return f(gn, v)
    }
}

filterArray(a, partialApply(isGoodNumber, GoodFactor))

Promise

Promise 对应的是前端应用的异步运行机制,为解决程序异步运行而创建出的特性。

创建、使用、串联 Promise 和错误捕获

首先,看一个使用回调函数的例子:

function add(
    a:number, b:number,
    callback: (res: number) => void): void {
        // 模拟网络请求的延迟
        setTimeout(()=>{
            callback(a+b)
        }, 2000)
}

add(2,3, res => {
    console.log('2+3', res)
    add(res, 4, res2 => {
        console.log("2+3+4", res2)
    })
})

这是一个使用 嵌套的回调函数 的例子。模拟网络请求的延迟,嵌套累加遍历。这种写法看起来:

  • 比较难懂
  • 当嵌套层数比较深的时候,不容易理解

下面把它改成 Promise 形式:

  • Promise 比回调函数更容易理解
  • 有更简单的使用和处理方式
  • 通过 .then 串联,解决繁杂的回调嵌套
  • 通过 reslovereject 返回成功或失败的情况
  • 通过 catch 捕获错误
function add( a:number, b:number): Promise<number> {
    return new Promise((reslove, reject) => {
        if (b%17===0) {
            // 定义处理失败情况
            reject(`bad number: ${b}`)
        }
        setTimeout(()=>{
            // 处理成功 返回 reslove
            reslove(a+b)
        }, 2000)
    })
}

// 通过 .then 调用 好理解 同时易于串联处理
add(2,3).then(res=>{
    console.log('2+3', res)
    return add(res, 17)
}).then(res=>{
    console.log('2+3+4', res)
}).catch(err=>{	// 通过 catch 捕获 reject 的情况
    console.log('caught error', err)
})

Promise 的定义如下:

var Promise: PromiseConstructor
new <number>(executor: (resolve: (value: number | PromiseLike<number>) => void, reject: (reason?: any) => void) => void) => Promise<number>

Promise 处理成功,以 resolve 返回,如果失败则以 reject 返回。

等待 Promise 运行

所有 Promise 都运行完成再进行下一步

function add( a:number, b:number): Promise<number> {
    return new Promise((reslove, reject) => {
        if (b%17===0) {
            reject(`bad number: ${b}`)
        }
        setTimeout(()=>{
            reslove(a+b)
        }, 2000)
    })
}
const p = Promise.all([add(2,3), add(4,5)]).then(res=>{
    console.log(res[0]*res[1]) 
})

通过 Promise.all(),可以等待参数中的 Promise 都运行完成再进行下一步操作,Promise.all() 会根据不同的参数形成签名,在这个例子中,的签名是:

PromiseConstructor.all<[Promise<number>, Promise<number>]>(values: [Promise<number>, Promise<number>]): Promise<[number, number]>

它接收了 Promise<number>类型的数组,并返回 number 类型的数组。

等待多个 Promise 中的一个完成就继续

const r = Promise.race([add(2,3), add(4,5)]).then(res=>{
    console.log(res) 
})

使用 Promise.race() 实现这个能力。当然它的返回结果只有一个。本示例中,它的函数签名如下:

PromiseConstructor.race<[Promise<number>, Promise<number>]>(values: [Promise<number>, Promise<number>]): Promise<number>

async/await 异步函数

asyncawait 算是 TS 中的一个语法糖,它们的使用可以很好的融入 Promise 语法,实现函数的异步运行:

function add( a:number, b:number): Promise<number> {
    return new Promise((reslove, reject) => {
        if (b%17===0) {
            reject(`bad number: ${b}`)
        }
        setTimeout(()=>{
            reslove(a+b)
        }, 2000)
    })
}
// 在声明函数之前 添加 async 关键字
// 用于异步执行
async function calc() {
    // await 不能在全局范围中使用 只能在局部中/函数中使用
    // 用于等待函数执行结果
    const a = await add(2,3)
    console.log('2+3', a)
    const b = await add(4,5)
    return b
}

calc().then(res => {
    console.log(res)
})

其中,函数 calc() 的声明如下:

function calc(): Promise<number>

它的返回值类型是一个 Promise<number> 。通过 asyncawait ,可以方便的使用 Promise 函数。

function add( a:number, b:number): Promise<number> {
    return new Promise((reslove, reject) => {
        if (b%17===0) {
            reject(`bad number: ${b}`)
        }
        setTimeout(()=>{
            reslove(a+b)
        }, 2000)
    })
}

function mul(a: number, b:number): Promise<number> {
    return new Promise((reslove, reject) => {
        reslove(a*b)
    })
}

async function calc() {
    // 同样的 使用 try-catch 捕获错误
    try {
        // 在这里依然可以使用 Promise.all() 因为它返回的仍然是 Promise
        const [a,b] = await Promise.all([add(2,3), add(4,5)])
        return await mul(a,b)
    } catch (err) {
        console.log('catch err', err)
        return undefined
    }
    
}

calc().then(res => {
    console.log(res)
})

接口

TS 将接口看为“一种对象形式的规定”,与其他语言不同,它不要求你“实现”一个接口,而是你定义的数据结构“要符合接口的规定”。接口描述一个类型应该有的字段。

  • readonly 表示只读字段
  • ? 用于标识可选字段
interface Employee {
    readonly name: string	// readonly 只读字段
    salary: number
    bonus?: number	// ? 可选
}

const e1: Employee = {
    name: 'jack',
    salary: 10000,
}

const e2: Employee = {
    name: 'jack',
    salary: 10000,
    bonus: 3000
}

接口里面可以定义方法吗?

当然可以。但是 TS 中的接口大多都只用于定义字段,它的设计并不适用于定义方法。

可选字段串联和非空断言

  • 使用 ? 标识可选字段,以及在使用时告诉编译器该字段可能没有;
  • 在使用变量时,利用 ! 进行可选字段的非空断言,让编译器认为该可选字段一定有值,忽略空的情况。

通过代码,这个特性还是比较好理解的:

interface Employee {
    name?: {
        first?: string
        last: string
    }
    salary: number
    bonus?: number
    updateBonus(p: number): void
}

function hasAAA(e: Employee) {
    return e.name?.first?.startsWith('AAA')
}

对于接口中定义的对象类型,也可以使用 ? 标识其可选性。在使用时,通过 field?. 让编译器自动处理字段空/非空的情况,本示例中,hasAAA 的函数签名为:

function hasAAA(e: Employee): boolean | undefined

如果,我们想告诉编译器,保证可选字段一定有值,那么就 使用 !,这样一来,如果出问题,例如实际上可选字段没有赋值,那么出现后果需要自己处理,编译器不会帮我们处理可选字段为空的情况:

function hasAAA(e: Employee) {
    return e.name!.first!.startsWith('AAA')
}

接口的扩展

接口之间可以相互扩展,使用 extends 关键字:

interface Employee extends HasName {
    salary: number
    bonus?: number
    updateBonus(p: number): void
}

interface HasName {
    name?: {
            first?: string
            last: string
        }
}

function hasAAA(e: Employee) {
    return e.name?.first?.startsWith('AAA')
}

类型的并、断言以及判断

类型的并

和 [Union of Types](#Union of Types) 一样,变量类型可以是多种接口的组合。在使用变量时,只能够 . 出来这些接口公有的字段。

interface Button {
    visible: boolean
    enabled: boolean
    onClick(): void
}

interface Image {
    visible: boolean
    src: string
}

function processElement(e: Button | Image) {
    // 只能 . 出来 visible 字段
    e.visible
}

使用 as 进行类型断言

上面代码中,如果我们像判断这个变量具体是符合哪一个接口的定义,就需要通过 as 关键字去实现类型断言,告诉编译器它是某个类型。

function processElement(e: Button | Image) {
    if ((e as any).onClick) {
        const btn = e as Button
        btn.onClick
    } else {
        const img = e as Image
        console.log(img.src)
    }
}

通过 as 关键字,告诉编译器变量的类型。

此外,还有一种语法也可以实现,即 is

function isButton(e: Button | Image): e is Button {
    return (e as any).onClick !== undefined
}

function processElement(e: Button | Image) {
    if (isButton(e)) {
        e.onClick
    } else {
        console.log(e.src)
    }
}

这两种方式本质上都是通过 (e as any).onClick 来判断是否有某个接口的属性,来实现接口断言。

TS 中的类与接口不同,类中的元素必须有初始化的值,通常有两种方式进行初始化

  • 直接使用 bonus: number = 0 为元素赋初始值。
  • 使用 constructor 构造函数为元素赋值。同时,在 constructor 中使用 publicprivate ,关键字,可以自动省略显式定义类的元素,相当于定义了相关字段。
  • 使用 ? 可选字段值不需要初始化
  • 默认字段都是 public
// 写法1
class Employee {
    name: string
    salary: number
    private bonus?: number
    // 可选字段 不需要初始化
    other?: string
    constructor(name: string, salary: number) {
        this.name = name
        this.salary = salary
    }
}

// 写法2
class Employee {
    private bonus?: number
    // 使用 public 和 private 后不需要声明
    // name: string
	// salary: number
    constructor(public name: string, private salary: number) {
        this.name = name
        this.salary = salary
    }
}

对于 private 对象,可以定义 setget 方法:

class Employee {
    private bonus?: number 
    constructor(public name: string, private salary: number) {
        this.name = name
        this.salary = salary
    }

    set setBonus(n: number) {
        this.bonus = n
    }
	// 如果直接返回 return this.bonus 这个函数签名就是 Employee.getBonus: number | undefined
    // 这里为了避免 undefined 的情况 在 bonus 不存在的情况下返回了 0
    // 此时函数签名为 Employee.getBonus: number
    get getBonus() { 
        return this.bonus || 0
    }
}

const e = new Employee('jack', 8000)
// 这里的调用就像赋值一样
e.setBonus = 10000
console.log(e)

类也可以继承:

  • 使用 super() 为其所继承的类赋值,注意这些函数签名的对应字段中不能有 publicprivate
class Employee {
    private bonus?: number 
    constructor(public name: string, public salary: number) {
        this.name = name
        this.salary = salary
    }

    set setBonus(n: number) {
        this.bonus = n
    }

    get getBonus() { 
        return this.bonus || 0
    }
}

const e = new Employee('jack', 8000)
e.setBonus = 10000
console.log(e)

class Manager extends Employee {
    private reporters: Employee[] = []
    // 这里的 name salary 前面不能有 public private
    // 因为是使用 super 赋值给所继承的类的值
    constructor (name: string, salary: number, public com: string) {
        super(name, salary)
        this.com = com
    }
    addReporter(e: Employee) {
        this.reporters.push(e)
    }
} 

用类实现接口

  • 推荐由接口的使用者去实现接口,使用接口的隐式实现

类如何实现接口?有两种方式:

  1. 通过显式的 implements 实现。

  2. 通过在类中,定义接口所拥有的所有变量,来隐式的实现。

interface Employee {
    name: string
    salary: number
}

// 或者省略 implements 关键字
class EmployeeImp implements Employee {
    private bonus?: number 
    constructor(public name: string, public salary: number) {
        this.name = name
        this.salary = salary
    }
}

const e: Employee = new EmployeeImp('jack', 8000)

泛型

泛型的定义是比较抽象的,它用来约束参数类型。例如:

const a: Array<number> = []
const p: Promise<number> = new Promise((reslove, reject)=>{
    ...
})

下面是自己定义泛型的一个示例:

class MyArray<T> {
    data: T[] = []
    add(t: T) {
        this.data.push(t)
    }
    map<U>(f: (v: T) => U): U[] { 
        return this.data.map(f)
    }
    print() {
        console.log(this.data)
    }
}

const a = new MyArray<number>()

a.add(1)
a.add(2)
// 编译器能够知道 此时 map 的类型是 MyArray<number>.map<string>(f: (v: number) => string): string[]
console.log(a.map(v=>v.toString()))
a.print()

用接口约束泛型的参数

interface HasWeight {
    weight: number
}

// 通过 extends 让泛型的类型中必须含有 HasWeight 接口中的字段
class MyArray<T extends HasWeight> {
    data: T[] = []
    add(t: T) {
        this.data.push(t)
    }
    map<U>(f: (v: T) => U): U[] { 
        return this.data.map(f)
    }
    print() {
        console.log(this.data)
    }
    sortByWeight() {
        this.data.sort((a,b)=>a.weight-b.weight)
    }
}

class WeightedNum {
    constructor(public weight: number) {
        this.weight = weight
    }
}

const a = new MyArray<WeightedNum>()

a.add(new WeightedNum(32))
a.add(new WeightedNum(11))
a.sortByWeight()
console.log(a)
Licensed under CC BY-NC-SA 4.0
最后更新于 Jan 15, 2023 15:57 CST
自认为是幻象波普星的来客
Built with Hugo
主题 StackJimmy 设计