前言
作为后端开发者,很多时候会面对前端开发的需求。就我最常用的 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(2,5) // 元素值 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
串联,解决繁杂的回调嵌套 - 通过
reslove
,reject
返回成功或失败的情况 - 通过
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 异步函数
async
和 await
算是 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>
。通过 async
和 await
,可以方便的使用 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
中使用public
或private
,关键字,可以自动省略显式定义类的元素,相当于定义了相关字段。 - 使用
?
可选字段值不需要初始化 - 默认字段都是
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
对象,可以定义 set
和 get
方法:
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()
为其所继承的类赋值,注意这些函数签名的对应字段中不能有public
和private
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)
}
}
用类实现接口
- 推荐由接口的使用者去实现接口,使用接口的隐式实现
类如何实现接口?有两种方式:
-
通过显式的
implements
实现。 -
通过在类中,定义接口所拥有的所有变量,来隐式的实现。
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)