0 概述
TypeScript编程读书笔记,这本书属于那种工具书,相当枯燥,但是非常全面,例子和语法很多,基本上把边边角角的都说清楚了。
2016时预计TypeScript为成为主流,而如今,连当年拥抱flow的Vue也不得不在Vue 3版本拥抱TypeScript了。不过直至如今,我才算是真正掌握了TypeScript。
TypeScript的最大特点是,它保持了js语法的动态性,同时提供了类型检查的支持,这一切都是建立在它灵活的编译时类型推导实现的,我觉得还是相当精彩巧妙的。另外,要注意的是,TypeScript是结构类型系统,不是声明类型系统(如Java,C++)
安装看这里,就不再重复了
1 基础类型
代码在这里
1.1 整数
function testNumber() {
let a = 1 // 推导为number类型
let b = 2.2 // 推导为number类型
const c = 1 // 推导为number为1的类型,注意这是字面值类型,const的类型收窄
const d: 1 = 1 // 可以在变量中指定类型
let e: number = 12
+= 1
a += 2
b += 3
e console.log(a, b, c, d, e)
}
注意有字面值类型,const默认会做类型收窄
1.2 字符串
function testString() {
let a = 'a' // 推导为string类型
const b = 'b' // 推导为string为b的类型,注意这是字面值类型,const的类型收窄
const c: 'ck' = 'ck' // 可以在变量中指定类型
let d: string = 'ge'
+= '1'
a += '3'
d console.log(a, b, c, d)
}
string类型
1.3 any类型
function testAny() {
let a: any = 3 // any类型,可以执行任何的操作
console.log(a)
+= 3 //
a = 'cd'
a = a.toUpperCase()
a console.log(a)
}
any类型仅仅是为了兼容js生态做出来的,尽可能不要在项目中使用any类型
1.4 unknown类型
function testUnknown() {
let a: unknown = 3 // unkown必须显示指定类型,ts不会推导出这个类型
// unknown的任何操作都是错误的,因为类型不知道,以下这一句会报错
// a.toUpperCase()
// 加上类型检查typeof的话,ts能在控制流中分析出a是string类型
if (typeof a === 'string') {
= a.toUpperCase()
a
}
console.log(a)
// 另外一种使用unknown的方法,是使用强制类型转换,强行告诉ts这个变量就是number类型,这样做是有运行时风险的
= (a as number) + 3
a
console.log(a)
}
unknown类型,表达了开发者暂时不知道这个类型,需要进行typeof或者instanceof检查后才能确定
1.5 bigInt类型
function testBigInt() {
// bigint是在ES2020才发布的
const a = 12n
const b = 23n
const c = a + b
console.log(a, b, c)
}
很少会用到,兼容性也不好,对浮点也没有支持,还不如用BigDecimal的库
2 对象类型
代码在这里,这里开始就容易迷惑了,要看清楚
2.1 object类型
function testObject() {
// object类型,可以用{},function和Array赋值
const a: object = {
: 3,
b
}const b: object = () => {
console.log('cc')
}const c: object = [1, 2, 3]
// 但是number和string是不能赋值给object类型的,以下两句会报错
// const d: object = 1
// const e: object = 'kk'
// object类型
console.log(a, b, c)
// object类型仅仅代表它是{},function或者Array,并不能拿到它的子段信息
// 例如,不能拿a.b成员
// console.log(a.b)
}
object仅仅代表它是一个对象,不能执行任何的方法,和成员访问
2.2 Object类型
function testObject2() {
// 尽量避免使用Object与{}类型
// Object与object是不同的,这一点真的很迷惑
// Object类型,可以用{},function和Array赋值
const a: Object = {
: 3,
b
}const b: Object = () => {
console.log('cc')
}const c: Object = [1, 2, 3]
// Object类型,也可以用number和string赋值
const d: Object = 1
const e: Object = 'kk'
// Object类型唯一不能赋值的是null和undefined
// const k: Object = null
// const j: Object = undefined
// Object类型
console.log(a, b, c, d, e)
// Object类型仅仅代表它是{},function或者Array,或者number,或者string,并不能拿到它的子段信息
// 例如,不能拿a.b成员
// console.log(a.b)
}
Object类型什么都能赋值,除了undefined,null,void和never类型
2.3 {}类型
function testObject3() {
// Object与{}类型几乎就是一个意思
// {}类型,可以用{},function和Array赋值
const a: {} = {
: 3,
b
}const b: {} = () => {
console.log('cc')
}const c: {} = [1, 2, 3]
// {}类型,也可以用number和string赋值
const d: {} = 1
const e: {} = 'kk'
// {}类型唯一不能赋值的是null和undefined
// const k: {} = null
// const j: {} = undefined
// {}类型
console.log(a, b, c, d, e)
// {}类型仅仅代表它是{},function或者Array,或者number,或者string,并不能拿到它的子段信息
// 例如,不能拿a.b成员
// console.log(a.b)
}
迷惑大赏开始了,{}类型与Object类型是相同的意思,但是它们与object类型是不同的!object类型是不能被number和string赋值的
2.3 具体的object类型
function testConcreteObject() {
// 声明一个a变量,它的类型是{label:string}
const a = {
: '34',
label
}console.log(a.label)
// 声明一个b变量,它的类型是{label:string}
// 我们也可以显式声明它的类型
let b: { label: string } = {
: '34',
label
}console.log(b)
// c的类型是{label:string,size:number}
const c = {
: '34',
label: 10,
size
}
// ts采取结构类型系统,c变量当然可以赋值给b类型{label:string}
// 结构类型系统不需要显式表现类型的关系,只需要类型可以满足要求即可,
= c
b
console.log(b)
// 声明k变量,但没有赋值
let k: {
: string
label
}
// 可以赋值
= { label: '12' }
k = { label: '78' }
k
// 对于立即创建的变量,即使满足类型约束,也不可以赋值,因为字段的数量必须完全相同
// 只能真的很费解FIXME
// k = { label: '23', size: 10 }
// 但是对于迂回创建的变量,只要满足类型约束,就可以赋值,即使字段数量更多
const k2 = { label: '23', size: 10 }
= k2
k
console.log(k)
}
终于到了具体的object类型,这个类型我们经常会用到。
3 类型操作符
作为一个类型推导系统,类型操作符当然是重点中的重点了
代码在这里
3.1 类型声明
function testTypeDeclarion() {
// 声明一个类型
type Age = number
// 使用这个类型来声明变量
let a: Age = 3
console.log(a)
// 再次声明一个类型,也是number类型
type Height = number
const b: Height = 4
// Height类型的变量b可以赋值给Age类型的变量a
// ts中依然是结构类型系统
= b
a
console.log(b)
}
类型声明,注意ts是结构性类型系统,不同类型之间也能赋值,只要满足类型约束就可以
3.2 类型或操作
function testTypeOr() {
// ts中类型是可以运算的
type Age = number
type Label = string
// Or运算允许取两个类型的并集
let a: Age | Label
// 因此变量a既可以被number,也可以被string赋值
if (Math.random() < 0.5) {
= 1
a console.log(a)
else {
} = '2'
a console.log(a)
}
// 但是a由于范围太大,无法调用number或者string的方法,所以以下这一句报错
// a.toUpperCase()
}
类型的或操作
function testTypeObjectOr() {
// ts中类型是可以运算的
type Dog = {
walk(): void
fire(): void
}type Cat = {
walk(): void
miao(): void
}
let a: Dog | Cat
// 因此变量a既可以被Dog,也可以被Cat赋值
const rand = Math.random()
if (rand < 0.5) {
= {
a : () => {
walkconsole.log('walk')
,
}: () => {
fireconsole.log('fire')
,
}
}else if (rand < 0.8) {
} = {
a : () => {
walkconsole.log('walk')
,
}: () => {
miaoconsole.log('miao')
,
}
}else {
} // a类型当然也可以是两个的组合
= {
a : () => {
walkconsole.log('walk')
,
}: () => {
fireconsole.log('fire')
,
}: () => {
miaoconsole.log('miao')
,
}
}
}
// 但是a由于范围太大,只能调用局部相同的方法,就是walk
.walk()
a }
我们常用的是对象类型的或操作,对象类型的或操作让可以被赋值的类型更多,但是可以执行的行为更少
3.3 类型与操作
function testTypeAnd() {
// ts中类型是可以运算的
type Age = number
type Label = string
// Or运算允许取两个类型的并集,显然任何值都取不了
let a: Age & Label
// 因此变量a既可以被number,也可以被string赋值
if (Math.random() < 0.5) {
// 任何值都取不了,报错
// a = 1
else {
} // 任何值都取不了,报错
// a = '2'
}
// console.log(a as unknown)
}
类型的与操作
function testTypeObjectAnd() {
// ts中类型是可以运算的
type Dog = {
walk(): void
fire(): void
}type Cat = {
walk(): void
miao(): void
}
// 是Dog与Cat的组合
let a: Dog & Cat
// 因此变量a只能被同时满足Dog,和Cat赋值
// 报错
/*
a = {
walk: () => {
console.log('walk')
},
fire: () => {
console.log('fire')
},
}
*/
/* 也报错
a = {
walk: () => {
console.log('walk')
},
miao: () => {
console.log('miao')
},
}
*/
// a类型只能是两个的组合
= {
a : () => {
walkconsole.log('walk')
,
}: () => {
fireconsole.log('fire')
,
}: () => {
miaoconsole.log('miao')
,
}
}
// 但是a可以被赋值的类型更少,所以能力更大
.walk()
a.fire()
a.miao()
a }
我们常用的是对象类型的与操作,对象类型的或操作让可以被赋值的类型更少,但是可以执行的行为更多
3.4 控制流类型推导
// HTMLInputElement是HTMLElement的子类
type Dog = {
: string
type: HTMLElement
labelwalk(): void
}
type Cat = {
: number
type: HTMLInputElement
labelwalk(): void
}
function testControlFlowInner(a: Dog | Cat) {
.walk()
aif (typeof a.type === 'string') {
// 因此在这里的话,ts在已知type为string的情况下
// 也只能推导出a.label的类型为HTMLElement|HTMLInputElement
console.log(a.label)
}
}
function testControlFlow() {
// 因为并集,可以是string的type,加上HTMLInputElement的label
testControlFlowInner({
: '1',
type: new HTMLInputElement(),
label: () => {
walkconsole.log('walk')
,
}
}) }
ts支持控制流的类型推导,它就像人一样推导当前的类型是什么
// HTMLInputElement是HTMLElement的子类
// 但是如果我们将type用字面值来表达的话
type Dog2 = {
: 'a'
type: HTMLElement
labelwalk(): void
}
type Cat2 = {
: 'b'
type: HTMLInputElement
labelwalk(): void
}
function testControlFlowInner2(a: Dog2 | Cat2) {
.walk()
aif (a.type === 'b') {
// ts能推导出来label是HTMLInputElement
console.log(a.label)
}if (a.type === 'a') {
// ts能推导出来label是HTMLElement类型
console.log(a.label)
}
}
// FIXME,我认为这里依然不是很严谨,具体看书P157
function testControlFlow2() {
// 因为并集,可以是string的type,加上HTMLInputElement的label
testControlFlowInner2({
: 'a',
type: new HTMLInputElement(),
label: () => {
walkconsole.log('walk')
,
}
}) }
迷惑的地方出现了,当一个类型的成员是字面值的时候,经过type操作判断,可以推导出了另外一个成员的类型。但是,对于成员是非字面值的时候,就无法推导另外一个成员的类型。
4 数组与元组类型
代码在这里
数组也是常用的一个类型,注意,数组也是属于object类型的
4.1 初始数组推导
function testArray() {
// 推导为number[]类型
const a = [1, 2, 3]
.push(3)
a// 下面这一句会自动报错,不能将string放入number[]类型
// a.push('4')
console.log(a)
// 可以显式加入类型
const b: string[] = ['1', '2', '3']
// 推导为 (number | string)[]类型
const c = [1, 'c']
console.log(b)
console.log(c)
}
对于数组有初始行为,或者有显式的数组类型时,ts会给出准确的数组类型
4.2 无初始数组推导
function testAutoDetectArray() {
// 直接声明时,类型为any[]
const a = []
.push(1)
a.push('2')
aconsole.log(a)
// 返回值推导为 (number | string)[]类型
return a
}
但是,对于数组没有初始数据时,ts会推导为any[]类型。然后,会在最终流程将所有数据组合起来分析,这个数组应该是什么类型。(这点也牛逼)
4.3 元组类型
function testTuple() {
// 元组,可以修改,但是将
let a: [number, string, string]
= [1, '2', '3']
a 0] = 4
a[
// 不能将类型不符的元组赋值过去,这样会报错
// a = ['1', '2', '3']
// 不能将其他长度的元组赋值过去,这样会报错
// a = [1, '2', '3', '5']
// 不能读取超越长度的index
// console.log(a[3])
// 可以推入新的元素进去,然后删除元素
// 这里的设计并不好
.push(4)
a.splice(0, 2)
a }
元组类型其实就是固定长度的数组,而且它每个位置固定数据类型。但是,ts竟然支持元组的push和splice操作,这点也是较为迷惑
5 枚举
代码在这里
枚举类型,可能是typescript的唯一败笔,我们来看看为什么这么说。
5.1 整数枚举
enum NormalEnum {
,
Red,
Green
}function testEnum() {
const a = NormalEnum.Red // 这个值为0
const b = NormalEnum[0] // 这个值为Red
const c = NormalEnum[1] // 这个值为Green
const d = NormalEnum[2] // 这个值为undefined,TS并没有在编译时报错,这个enum的设计并不好
console.log(a, b, c, d)
}
默认的枚举是整数值,所以我们能用整形来索引得到枚举,但是即使传入的数字不是有效值,ts也不会报错
5.2 常量整数枚举
// 枚举值默认是从0开始
const enum NormalEnum2 {
,
Red,
Green
}
function testEnum2() {
const a = NormalEnum2.Red // 这个值为0
// 使用了const enum以后,只能用string来访问enum。不能用整数来访问enum
// 以下都会报错
// const b = NormalEnum2[0]
// const c = NormalEnum2[1]
// const d = NormalEnum2[2]
console.log(a)
}
// 枚举值默认是从0开始
const enum NormalEnum3 {
,
Red,
Green
}
function testEnum3Inner(a: NormalEnum3) {
console.log(a)
}
function testEnum3() {
// 把其他枚举体传过去是不行的,会报错,即使值一样,枚举不是结构类型
// testEnum3Inner(NormalEnum.Red)
// 安全可靠,值为NormalEnum
testEnum3Inner(NormalEnum3.Red)
// 这里竟然能将整数当枚举传递过去,一点都安全,根本就没有整数为200的枚举
testEnum3Inner(200)
}
使用了常量整数枚举以后,避免了整数索引枚举的问题,只能用字符串来索引枚举。但是,当枚举类型作为函数参数的时候,竟然又可以用不合法的整数值来传递了。
这个设计,真的是,一言难尽。
5.3 常量字符串枚举
// 这个写法最好,但是对开发者要求太高
const enum NormalEnum4 {
= 'red',
Red = 'green',
Green
}
function testEnum4Inner(a: NormalEnum4) {
console.log(a)
}
function testEnum4() {
// 安全可靠,值为NormalEnum
testEnum4Inner(NormalEnum4.Red)
// 要将枚举的值手动全部改为string,才是安全的
// testEnum4Inner(200)
// 手动传入字符串也不行
// testEnum4Inner('red')
}
只有使用常量字符串枚举的时候,这个才是可靠的。但是,ts的枚举竟然支持字符串枚举,与整数枚举的混合写法。
const enum NormalEnum4 {
= 'red',
Red = 123,
Green }
如果这样写代码的话,那么之前出现的问题还会重新出现一次。没有编译时的报错,也没有ESLint的报错,这种属于悄悄地发生错误,一点都不可靠。
5.4 字面值常量
type NormalEnum5 = 'red' | 'green'
function testEnum5Inner(a: NormalEnum5) {
console.log(a)
}
function testStringAsEnum() {
// 传其他字符串也行
testEnum5Inner(NormalEnum4.Red)
// 支持手动传入字符串
testEnum5Inner('red')
// 传入整数当然是不行的
// testEnum5Inner(200)
}
使用字面值常量代替枚举才是正确的用法,完全不用担心编译时传错了数据的问题。而且,字面值常量还支持in ,keyof等Mapped Type特性,还有自动的全面性检查的特性,非常好用,枚举就没有这点好处了,后面会介绍到这点。
强烈要求禁止使用枚举,而改用字面值常量来代替枚举。
6 函数类型
代码在这里
函数类型,在js和ts中它是属于Object类型。并且,在js中,函数甚至还能有自己的成员变量与方法。这一点ts也保留下来了。
6.1 基础
6.1.1 普通函数
// 普通命名函数
function go1(name: string) {
console.log(`go:${name}`)
}
// 匿名函数赋值给变量
const go2 = (name: string, age: number) => {
console.log(`name:${name},age:${age}`)
}
//创建一个function type
type goType = (a:string,b:number)=>void
function testFunction() {
go1('a')
go2('fish', 200)
let a:goType = go2
}
命名与匿名函数,简单,注意fuction type类型的声明
6.1.2 默认参数与可选参数
// 默认参数与可选参数
function go3(message = 'Hello', size?: number) {
console.log(`message,${message},size:${size}`)
}
function testFunction2() {
go3()
go3('uu')
go3('uu', 10)
}
默认参数与可选参数
6.1.3 不定参数
// 不定参数的写法
function go4(format: string, ...args: any[]) {
console.log(format, ...args)
}
function testFunction3() {
go4('a')
go4('b', 1, '2')
go4('c', 1, 'bc', 'g')
}
不定参数
6.1.4 this参数指定
type MyGo = {
walk(): string
}
// 注解this参数
// 函数的返回值标注
function go5(this: MyGo, message: string): string {
return `{${this.walk()},${message}}`
}
function testFunction4() {
// 编译错误,因为this参数为null
// 直接调用函数时,this参数取的是外部环境的this
// go5('ab')
const myObj = {
: (): string => 'mm',
walk
}// 用call方法调用函数,可以指定this
const result = go5.call(myObj, 'tt')
console.log(result)
}
函数其实都是有一个this参数的,一般情况传入的this的实际参数就是null,形式参数我们默认是没有标注的。但是在ts中,我们可以显式地标注这个参数,并且要求ts调用这个函数时,this参数都要满足我们指定的要求。
6.2 生成器与迭代器
生成器是一个可以生成数值的函数,它可以生成有限的数据,也可以生成无限的数据,这个在ts中是没有限制的。迭代器是一个对象,它含有一个值为生成器的属性。我们要注意两者的区别
6.2.1 生成器
// 生成器函数,签名上要有*方法,每次返回值用yield关键字
function* go6() {
let n = 10
while (n >= 0) {
yield n
-= 1
n
}
}
function testFunction5() {
const generator = go6()
// 每次拿值就用它的next方法,只要done为false就永远有值
// 生成器函数的问题在于,它不支持for in的方法
let singleResult = generator.next()
while (!singleResult.done) {
console.log(`generator :${singleResult.value}`)
= generator.next()
singleResult
} }
用function*指定的就是生成器函数,生成器函数都会自动带有一个next方法,来不断获取下一个数值
6.2.2 迭代器
// 这个是迭代器,不是生成器
function createGo7() {
// 迭代器是一个对象,它还有一个key为Symbol.iterator的属性,该属性的值为生成器函数
return {
*[Symbol.iterator]() {
let n = 10
let i = 0
while (n >= 0) {
yield [i, `value${n}`]
-= 1
n += 1
i
},
}
}
}
function testFunction6() {
const iterator = createGo7()
// 满足要求的迭代器,就可以使用for of语法来遍历
for (const [i, value] of iterator) {
console.log(i, value)
} }
迭代器是一个对象,它含有一个key为Symbol.iterator的属性,这个属性的值为生成器。满足这样约束的对象就是迭代器,于是就可以用普通的for in和for of来迭代这个对象了,这其实是一个语法糖
6.3 函数也是一个对象
function testObjectFunction1() {
// 我们可以用object的形式来声明一个函数类型
// 不要用in作为参数名,因为in是关键字
type Dog = {
: number): string
(mk
}
// 直接将函数赋值给这个Dog类型是没问题的
const a: Dog = function (mk: number): string {
return `${mk}bb`
}// 输出的是1bb
console.log(a(1))
// 输出的函数名字,就是a本身
console.log(a.name)
}
我们可以先声明一个object,它还有一个匿名的函数。那么我们可以用函数来赋值到这个object类型,是没问题的。
function testObjectFunction2() {
// 既然object可以表达function,那么它也可以带自己的字段和成员
type Dog = {
: string
()next(): string
: number
size
}
function mm(): string {
return 'Hello World'
}.next = () => '123'
mm.size = 100
mm
const a: Dog = mm
a()
console.log(a.next())
console.log(a.size)
}
甚至我们可以声明一个称为Dog的类型,它有方法和成员,也有一个匿名的函数。那么这个Dog的类型,完全是可以用一个命名函数去赋值它的,所以能看到函数也是可以有自己的成员变量和方法的
6.4 函数重载
// 一个函数的声明,代表该函数的重载
function GoFishing(input: number): string
function GoFishing(input: string): string
function GoFishing(): string
// 对这个函数的实现,只能有一个实现
function GoFishing(input?: number | string): string {
if (typeof input === 'number') {
return 'mm'
}return 'gg'
}
function testOverloadFunction1() {
// 定义一个固定名字的函数
const result1 = GoFishing(1)
const result2 = GoFishing('2')
const result3 = GoFishing()
console.log(result1, result2, result3)
}
函数重载是多个函数声明,一个函数实现。注意的是,同一个函数的多个函数重载的时候,只能有一个杉树实现,该实现必须满足这个函数的多个声明的约束。
function testOverloadFunction2() {
// 定义函数的类型,有多个声明,表明是可重载
type MyOverloadFunc = {
: number): string
(input: string): string
(input
}
// 实现这个函数的重载实现,注意,与其他类型不同的,函数可以有多个声明,但只能有一个实现
const callFunc: MyOverloadFunc = function (input: number | string): string {
if (typeof input === 'number') {
return `mm${input}`
}return `cc${input}`
}
const result1 = callFunc(1)
console.log(result1)
const result2 = callFunc('fish')
console.log(result2)
}
另外一种声明函数的方法,是以object的方式来声明函数重载,只是换个写法而已。
6.5 泛型函数(多态)
ts也支持泛型函数,而且结合类型的推导,与函数重载,他的功能会更加强大
6.5.1 不同的字面值输入,输出不同的类型
class Dog {}
class Cat {}
class Fish {}
function GoPlay(input: 'a'): Dog
function GoPlay(input: 'b'): Cat
function GoPlay(input: 'c'): Fish
function GoPlay(input: string): Dog | Cat | Fish {
if (input === 'a') {
return new Dog()
}if (input === 'b') {
return new Cat()
}if (input === 'c') {
// 这个实现并不可靠,因为实现
// 只能返回new Dog()依然能编译成功
return new Fish()
}throw new Error('mm')
}
function testGenericFunction1() {
// 定义一个固定名字的函数,在编译时就能推导出它的结果
const result1 = GoPlay('a')
const result2 = GoPlay('b')
const result3 = GoPlay('c')
console.log('GoPlay', result1, result2, result3)
}
例如GoPlay函数,就是输入为不同的字面值类型,输出为不同的类型。我们希望在编译时就能计算这种不同的结果,以避免运行时的错误(例如,输入参数b,但是得到返回值以后,调用它的Dog的方法)。这点,我们可以通过函数重载来实现
// 使用keyof的实现也并不可靠
type Dance = {
: Dog
a: Cat
b: Fish
c
}function GoDance<T extends keyof Dance>(input: T): Dance[T] {
if (input === 'a') {
return new Dog()
}if (input === 'b') {
return new Cat()
}if (input === 'c') {
// 这个实现并不可靠
// 只能返回new Dog()依然能编译成功
return new Fish()
}throw new Error('mm')
}
function testGenericFunction2() {
// 定义一个固定名字的函数,在编译时就能推导出它的结果
const result1 = GoDance('a')
const result2 = GoDance('b')
const result3 = GoDance('c')
console.log('GoDance', result1, result2, result3)
}
另外一种方法是用keyof和泛型来实现,这是一个相当重要的技巧。在书本的P208中有大量的使用这种技巧来构建安全的异步和回调方法
6.5.2 不同的类型输入,输出不同的类型
class Dog {}
class Cat {}
class Fish {}
function GoPlay(input: number): Dog
function GoPlay(input: string): Cat
function GoPlay(): Fish
function GoPlay(input?: number | string): Dog | Cat | Fish {
if (typeof input === 'number') {
return new Dog()
}if (typeof input === 'string') {
// 这个实现并不可靠
// 只能返回new Dog()依然能编译成功
return new Cat()
}return new Fish()
}
function testGenericFunction1() {
// 定义一个固定名字的函数,在编译时就能推导出它的结果
const result1 = GoPlay(1)
const result2 = GoPlay('b')
const result3 = GoPlay()
console.log('GoPlay', result1, result2, result3)
}
我们可以用函数重载来实现,不同的类型输入,有不同的类型输出。而且,这些类型输出在编译时就能推导出来了。
// 使用keyof无法实现以上的功能,要用泛型判断
type DanceResultType2<T> = T extends number ? Dog : Cat
type DanceResultType<T> = T extends undefined | null | never | void
? Fish
: DanceResultType2<T>
function GoDance<T extends number | string>(input?: T): DanceResultType<T> {
if (typeof input === 'number') {
return new Dog()
}if (typeof input === 'string') {
// 这个实现并不可靠
// 只能返回new Dog()依然能编译成功
return new Cat()
}return new Fish()
}
function testGenericFunction2() {
// 定义一个固定名字的函数,在编译时就能推导出它的结果
const result1 = GoDance(1)
const result2 = GoDance('b')
// 这里推导不好,只能推导出Dog|Cat类型,而不是Fish类型
const result3 = GoDance()
console.log('GoDance', result1, result2, result3)
}
如果用泛型实现的话,就不能用keyof操作符了,因为输入的参数类型不是字面值类型,是不同的类型。这个时候,要用类型判断表达式。但是以上的实现中,有点遗憾的是,对于无参数的输入,推导出来的返回值类型是不对的。
// 使用泛型加重载就能解决这个问题了
type FuckResultType<T> = T extends number ? Dog : Cat
function GoFuck<T extends number | string>(input: T): FuckResultType<T>
function GoFuck<T>(): Fish
function GoFuck<T>(input?: T): Fish | FuckResultType<T> {
if (typeof input === 'number') {
return new Dog()
}if (typeof input === 'string') {
return new Cat()
}return new Fish()
}
function testGenericFunction3() {
// 定义一个固定名字的函数,在编译时就能推导出它的结果
const result1 = GoFuck(1)
const result2 = GoFuck('b')
// 现在能推导出来result3是Fish类型了
const result3 = GoFuck()
console.log('GoFuck', result1, result2, result3)
}
我们可以用泛型+重载的方法来解决这个问题,这个时候无参数的输入,推导出来的返回值类型是正确的
6.5.3 不同的类型输入,输出不同的字面值
function GoPlay(input: number): 'mm'
function GoPlay(input: string): 'gg'
function GoPlay(input: object): 'kk'
function GoPlay(): 'll'
function GoPlay(input?: number | string | object): 'mm' | 'gg' | 'kk' | 'll' {
if (typeof input === 'number') {
return 'mm'
}if (typeof input === 'string') {
return 'gg'
}if (input instanceof Object) {
return 'kk'
}return 'll'
}
function testGenericFunction1() {
// 定义一个固定名字的函数,在编译时就能推导出它的结果
const result1 = GoPlay(1)
const result2 = GoPlay('b')
const result3 = GoPlay()
const result4 = GoPlay({})
console.log('GoPlay', result1, result2, result3, result4)
}
同样地,我们可以用函数重载来实现,不同的类型输入,能推导出不同的字面值类型
// 使用泛型加重载就能解决这个问题了
type FuckResultType<T> = T extends number
? 'mm'
: T extends string
? 'gg'
: 'kk'
function GoFuck<T extends number | string | object>(input: T): FuckResultType<T>
function GoFuck<T>(): 'll'
function GoFuck<T>(input?: T): 'll' | FuckResultType<T> {
if (typeof input === 'number') {
return 'mm' as FuckResultType<T>
}if (typeof input === 'string') {
return 'gg' as FuckResultType<T>
}if (input instanceof Object) {
return 'kk' as FuckResultType<T>
}return 'll'
}
function testGenericFunction2() {
// 定义一个固定名字的函数,在编译时就能推导出它的结果
const result1 = GoFuck(1)
const result2 = GoFuck('b')
const result3 = GoFuck()
const result4 = GoFuck({})
console.log('GoFuck', result1, result2, result3, result4)
}
同样地,用函数泛型+函数重载也能解决这个问题
7 类
代码在这里
原来的js系统中是没有类这个说法,类仅仅是通过对象和函数模拟的。要注意的是,在ts与js中,类不仅是一个类型,它本身还是一个值(你可以将类本身赋值给一个变量)。这跟Java和Golang都不同。
7.1 基础
7.1.1 构造器,继承,方法与成员
class Game {
// 构造器含有private,protected,public的表明它不仅是构造器参数,还会自动被赋值为成员
constructor(private isStart: boolean, private playerSize: number) {}
// 方法的定义
public getIsStart() {
return this.isStart
}
public getPlayerSize() {
return this.playerSize
}
}
// 继承用,extends
class PieceGame extends Game {
constructor(
public pieceSize: number,
: boolean,
isStart: number
playerSize
) {// 调用父类的构造器
super(isStart, playerSize)
}
}
class AnimalGame extends Game {
// 构造器的animalSize是没有访问标志的,没有public,没有private,也没有protected,所以这个仅仅是构造器参数,不是成员
constructor(animalSize: number, isStart: boolean, playerSize: number) {
super(isStart, playerSize)
}
}
function testClass1() {
const game = new Game(true, 10)
// isStart是private访问,不能获取
// console.log(game.isStart)
console.log(game.getIsStart()) // 这个值为true
console.log(game.getPlayerSize()) // 这个值为10
const pieceGame = new PieceGame(10, false, 70)
// pieceSize是public权限,所以可以访问
console.log(pieceGame.pieceSize)
console.log(pieceGame.getIsStart())
console.log(pieceGame.getPlayerSize())
const animalGame = new AnimalGame(10, true, 80)
// animalSize是不是成员,它仅仅是个构造器参数而已
// console.log(animalGame.animalSize)
console.log(animalGame.getIsStart())
console.log(animalGame.getPlayerSize())
}
注意ts中的特殊写法,如果构造器上有访问标志,那么这个参数就是成员变量的一部分
7.1.2 抽象类
// 抽象类
abstract class Animal {
// 抽象类也可以有自己的方法
public go() {
if (this.canGo() === false) {
console.log('this animal can not walk!')
return
}console.log('animal walk')
}
abstract canGo(): boolean
}
class Dog extends Animal {
public canGo() {
return true
}
}
class Fish extends Animal {
public canGo() {
return false
}
}
function testClass2() {
// 抽象类不能实例化
// const animal = new Animal()
// 非抽象类才可以实例化
const dog = new Dog()
.go()
dog
const fish = new Fish()
.go()
fish }
抽象类不能实例化,但是它可以带具体的方法
7.2 接口
7.2.1 接口实现
interface Walker {
walk(): void
}
// 接口实现用implements而不是extends
class Dog implements Walker {
public walk() {
console.log('dog walk')
}
}
class Cat implements Walker {
public walk() {
console.log('cat walk')
}
}
function testClassInterface1() {
const a = new Dog()
.walk()
a
const b = new Cat()
.walk()
b }
接口的基础使用,指定一个类实现了某个接口,用implements
7.2.2 接口与类型
type MK = {
swim(): void
}
// 在ts中,你甚至可以用type来作为类的implements指定,像interface一样
class Fish implements MK {
swim(): void {
console.log('fish swim')
}
}
interface Swimer {
swim(): void
}
function testClassInterface2() {
const a = new Fish()
.swim()
a
// 即使Fish类型没有显式实现Swimer接口,也可以赋值给Swimer接口,结构性类型系统
// implements其实是一种类型的声明性约束,在语法层要求代码实现了这个接口而已
const b: Swimer = a
.swim()
b }
接口其实只是类型的一种语法糖,对一个类标志implements 某个接口,仅仅为告诉编译器,帮我检查一个这个类,确保它满足了这个接口而已。这个类的实例依然可以赋值到其他没有显式标志的接口。
7.2.3 接口派生
// SwimerAndWalker接口同是extends了两个
interface SwimerAndWalker extends Swimer, Walker {}
// type类型的话,可以用&运算来模拟extends的接口
type MM = MK & Walker
class Frog implements Swimer, Walker {
public swim(): void {
console.log('frog swim')
}
public walk(): void {
console.log('frog walk')
}
}
function testClassInterface3() {
// 赋值失败,因为Fish只会Swim,不会Walk
// const a: SwimerAndWalker = new Fish()
const a: SwimerAndWalker = new Frog()
const b: MM = new Frog()
.swim()
a.walk()
a
.swim()
b.walk()
b }
我们可以指定组合多个接口为一个新接口,它就像可以对多个类型执行and操作生成一个新类型一样。
7.3 类也是一个值
ts的类,不仅是一个类型,而且还是一个值,这被称为伴生对象模式P170.
class Dog {
// 属性默认为public
: string
nameconstructor(label: string) {
this.name = label
}
static showName: string = 'I am Dog Name'
static getGlobalSize(): number {
return 10
}
}
function testClassFunction1() {
const dog = new Dog('mm')
console.log(dog.name)
// Dog的静态变量,与静态方法
console.log(Dog.showName)
console.log(Dog.getGlobalSize())
// 静态方法不能通过实例来拿取
// console.log(dog.showName)
}
我们可以指定类的静态方法与成员
// 在ts的实现来看,Class其实是一个Object,一个含有new方法的Object而已
type MyType = {
new (...args: any[]): object
: string
showNamegetGlobalSize(): number
}function testClassFunction2() {
// 看,Dog类本身就是一个实例,它可以赋值给一个Type
const a: MyType = Dog
console.log(a.showName)
console.log(a.getGlobalSize())
// 要使用MyType的new方法,就需要先把它extends了,然后创建这个对象出来
class MM extends a {
constructor(...args: any[]) {
super(...args)
}
}
// 创建了这个对象
const mm = new MM('gg')
// 可以拿到这个MM底层对应的name
console.log((mm as Dog).name)
}
而类本身也可以赋值给另外一个类型,甚至作为值本身的类,可以动态地为他扩展其他方法与成员
7.4 装饰器
class Builder {
private data: object | null = null
private method: 'get' | 'post' | null = null
private url: string | null = null
public name: string
constructor(name: string) {
this.name = name
}
public setMethod(method: 'get' | 'post'): this {
this.method = method
return this
}
public setData(data: object): this {
this.data = data
return this
}
public setURL(url: string): this {
this.url = url
return this
}
public send() {
console.log('send', this.method, this.url, this.data)
}
}
type ClassConstructor<T> = new (...args: any[]) => T
function decorator<T extends ClassConstructor<{}>>(Constructor: T) {
return class extends Constructor {
constructor(...args: any[]) {
super(...args)
}
go() {
console.log('mm')
}
}
}
export default function testDecorator() {
const builder = new Builder('mm')
.setData({ a: 3 }).setMethod('get').setURL('www.baidu.com').send()
builder
const constructor = decorator(Builder)
// 注意newData的类型为decorator的匿名class&Builder
const newData = new constructor('mj')
.go()
newData }
当理解到了类其实是一个值的时候,装饰器模式就容易理解了
7.5 this参数指定
// 需要两个类型,T类型为object,K类型必须来自于T类型的key
type Picker<T, K extends keyof T> = {
// 对于K类型的所有值,建立一个字段映射类型
in K]: T[key]
[key
}
// Picker<BuildableRequest,'method'>,相当于声明{'method':'get'|'post'}类型
// TS中的类型是结构类型,不是声明类型,只要类型中实现了这个接口结构,那么类型就可以实现了这个接口。
interface BuildableRequest {
: boolean
hasData: boolean
hasMethod: boolean
hasUrl
}
// 例如一个Picker<BuildableRequest,'hasMethod'>&Picker<BuildableRequest,'hasData'>&Picker<BuildableRequest,'hasUrl'>,就代表它实现了这个BuildableRequest接口
class Builder {
?: object
data?: 'get' | 'post'
method?: string
url
public setMethod(
: 'get' | 'post'
method: this & Picker<BuildableRequest, 'hasMethod'> {
)return Object.assign(this, { method: method, hasMethod: true })
}
public setData(data: object): this & Picker<BuildableRequest, 'hasData'> {
return Object.assign(this, { data: data, hasData: true })
}
public setURL(url: string): this & Picker<BuildableRequest, 'hasUrl'> {
return Object.assign(this, { url: url, hasUrl: true })
}
// 重写this参数,this需要为Builder,同时也要满足BuildableRequest
public send(this: BuildableRequest & Builder) {
console.log('send', this.method, this.url, this.data)
return this
}
}
export default function testChangeClassThis() {
const builder = new Builder()
.setData({ a: 3 }).setMethod('get').setURL('baidu.com').send()
builder
.setData({ a: 3 }).setMethod('get').setURL('baidu.com').send()
builder
// 缺少任意一个set,ts都会报错
/*
builder.setData({a:3})
.setMethod('get')
.setURL("baidu.com")
.send();
*/
}
方法中的this参数我们是默认不写的,当然我们也可以显式指定this参数的约束,从而让ts编译器保证调用这个方法需要满足什么条件。
7.6 this与方法
class User {
private name: string
constructor(name: string) {
this.name = name
}
// 直接声明的方法
walk() {
console.log((this ? this.name : '') + 'walk')
}
// 使用箭头声明的方法,默认就会绑定当前的this
= ():number => {
jump console.log(this.name + 'jump')
return 123;
}
}
let gg:Person | undefined;
abstract class Person{
constructor(){
console.log('constructor begin --- ');
//能调用得到子级Man.walk.
this.walk();
//失败,这个时候调用的是父级的jump
this.jump();
console.log('constructor end --- ');
= this;
gg
}
//直接声明的方法,没有箭头的函数,
walk(){
console.log('person walk');
}
//有箭头的函数
//加入private修饰,我们能避免jump方法被override,TS没有提供final的修饰符
= ()=>{
jump console.log('person jump');
}
}
/*
箭头函数的实现,运行时定义函数。没有箭头函数的实现,一开始就定义在protype链的函数
class Person {
constructor() {
this.jump = () => {
console.log('person jump');
};
console.log('constructor begin --- ');
...
}
walk() {
console.log('person walk');
}
}
*/
class Man extends Person{
constructor(){
super();
}
// 直接声明的方法
walk() {
override super.walk();
console.log('man walk');
}
//有箭头的函数,override只检查声明,没有检查箭头的问题
= ()=>{
jump //调用super会失败,箭头函数里面会丢失super
//super.jump();
console.log('man jump');
}
}
/*
Man.jump箭头函数的实现,在super运行完成以后,才去定义this.jump.
class Man extends Person {
constructor() {
super();
this.jump = () => {
//调用super会失败,箭头函数里面会丢失super
//super.jump();
console.log('man jump');
};
}
walk() {
super.walk();
console.log('man walk');
}
}
*/
function testClassMethodThis() {
const a = new User('fish')
// 这样的话附带有this
.walk()
a
// 这样做是不安全的,会丢失this指针
// 但是,TS并没有报错,相当诡异
const b: () => void = a.walk
b()
// 这样的话也附带有this
.jump()
a
// 即使只取方法,也有this,因为是箭头函数
const c: () => void = a.jump
c()
const man = new Man();
console.log('constructor outer begin ---');
.walk();
man.jump();
manconsole.log('constructor outer end ---');
?.jump();
ggconsole.log('gg end ---');
}
export default testClassMethodThis
要点如下:
- 箭头方法默认是绑定了this,而非箭头方法则受限于调用时的方式,有可能会丢失this
- 箭头方法的缺点在于,不能调用super,在基类构造的时候,无法调用子类的同名方法。
换句话说,一旦使用箭头方法以后,就失去了继承方法的灵活性。所以,我们推荐,箭头只提供在对外被绑定的事件上,并且最好加上private修饰,从而避免被子类override.
7.7 泛型
7.7.1 泛型类
class Stack<T> {
private data: T[]
constructor() {
this.data = []
}
public push(single: T): void {
this.data.push(single)
}
public pop(): T {
if (this.data.length === 0) {
throw new Error('stack is empty')
}const result = this.data[this.data.length - 1]
this.data.splice(this.data.length - 1, 1)
return result
}
}
function testGeneric1() {
const numberStack = new Stack<number>()
.push(1)
numberStack.push(2)
numberStack
console.log(numberStack.pop())
console.log(numberStack.pop())
try {
console.log(numberStack.pop())
catch (e) {
} console.log('pop fail')
} }
泛型类的例子,还是相当简单的
7.7.2 泛型类下的泛型方法
class ObjectWrapper<T extends object> {
private data: T
constructor(data: T) {
this.data = data
}
public get<G extends keyof T>(single: G): T[G] {
return this.data[single]
}
}
function testGeneric2() {
const a = new ObjectWrapper({
: 'fish',
name: 2,
size
})// 自动推导result1的类型为string,result2的类型为number
const result1 = a.get('name')
const result2 = a.get('size')
console.log(result1, result2)
}
泛型类下的泛型方法,方法也是可以带上泛型的,这样做更加安全
8 高级特性
代码在这里
8.1 全面性检查
type Animal = 'dog' | 'cat' | 'fish'
function getAnimalOrdal(input: Animal): number {
// 这样写不好,会漏掉fish的常量
let number = 0
if (input === 'dog') {
= 1
number else if (input === 'cat') {
} = 2
number
}return number
}
function getAnimalOrdal2(input: Animal): number {
// 这样写是最好的,漏掉一个情况,都会报错
switch (input) {
case 'dog':
return 1
case 'cat':
return 2
case 'fish':
return 3
}
}
function testFullCheck() {
getAnimalOrdal('dog')
getAnimalOrdal2('cat')
}
switch语句有自动的全面性检查分析,保证我们不会漏掉其中一个分支,相当可靠
8.2 MappedType
MappedType的意思是,将一个类型为依据,转换到另外一个类型,这是类型运算的另外一种操作符,有时候相当有用省事
8.2.1 in操作符
type Day = 'Mon' | 'Tues' | 'Wes'
type Result = 12 | 34
// mappedType是ts弥补静态类型的方法,它的意思是创建一个类型,该类型含有Day的所有key,Value类型必须为Result类型
// in 操作符的意思是,将Day这种字面值类型并集的每一个元素,拿出来作为object类型的key类型
type MyMappedType = {
in Day]: Result
[K
}
function testMappedType1() {
const a: MyMappedType = {
: 34,
Mon: 12,
Tues// 如果缺少一个成员,都会报错
: 12,
Wes
}console.log(a)
}
使用in操作符,我们可以将字面值常量并集,转换为object类型的key
// 建立一个MyRecord类型,它其实就是原生Record类型的实现
// 就是刚才的MyMappedType类型的泛型版本而已
type MyRecord<T extends keyof any, U> = {
in T]: U
[K
}
function testMappedType2() {
const a: MyRecord<Day, Result> = {
: 34,
Mon: 12,
Tues// 如果缺少一个成员,都会报错
: 12,
Wes
} }
in操作符也支持泛型下的使用
8.2.2 keyof操作符
type Account = {
: number
id: boolean
isEmployee: string[]
notes
}
// in 操作符是将字面值类型的并集转换为object的key类型
// 那么keyof 操作符就是反过来,将object的key类型转换为字面值类型的并集
type AccountKey = keyof Account
function testMappedType3() {
let a: AccountKey
= 'id'
a = 'isEmployee'
a = 'notes'
a // 这里会报错,因为'id3'不是Account的key类型,不在AccountKey字面值中
// a = 'id3'
console.log(a)
}
keyof操作符就是反过来,它将object的key类型,转换为字面值常量的并集
8.2.3 in与keyof操作符组合
// 结合in操作符,与keyof类型,我们可以做到将object类型,转换为另外一种object类型
// 现在AccountOption类型的每个字段都是可选的
type AccountOption = {
in keyof Account]?: Account[K]
[K
}
function testMappedType4() {
// Account类型的每个字段都是必选的
const a: Account = {
: 1,
id: true,
isEmployee: ['fish'],
notes
}console.log(a)
// AccountOption类型的每个字段都是可选的
const b: AccountOption = {
: 1,
id: true,
isEmployee
}console.log(b)
}// 同理,还有预定义好的方法,例如Record,Partial,Required,ReadOnly和Pick等等
组合in与keyof操作符,我们可以以对象类型为依据,转换为另外一个对象类型
8.2.4 typeof 操作符
const MyAccount = {
a:()=>{
return "123"
},
b:()=>{
return 123
}
}
//typeof 操作符,不仅可以在运行时使用,编译时也能用来使用
type MyAccountType = typeof MyAccount;
type MyAccountKeyType = keyof MyAccountType;
function testMappedType5(){
function ok(a:MyAccountKeyType){
console.log(a);
}
ok('a');
ok('b');
}
typeof操作符不仅可以在运行时使用,编译时也可以使用
8.3 条件类型
extends是一种类型的编译时条件选择操作
8.3.1 三元操作符
class NumberWrapper {}
class StringWrapper {}
// extends方式是一种编译时的条件类型判断
type WrapperResultType<T> = T extends number ? NumberWrapper : StringWrapper
function wrapper<T extends number | string>(input: T): WrapperResultType<T> {
if (typeof input === 'number') {
return new NumberWrapper()
else {
} return new StringWrapper()
}
}
function testTypeCheck1() {
// 编译时就可以进行类型的判断操作,这其实是函数重载的另外一种实现,但比函数重载强大多了
const result1 = wrapper(1)
const result2 = wrapper('hello')
console.log(result1, result2)
}
只支持三元操作符
8.3.2 元素类型推导
// infer U是一种特殊的类型判断,它可以提取数组中的元素类型
type ElemType<T> = T extends (infer U)[] ? U : T
function testTypeCheck2() {
// 可以看到R1,R2,R3的实际类型是啥
type R1 = ElemType<number>
type R2 = ElemType<number[]>
type R3 = ElemType<string[]>
}
配合infer操作符,可以在编译时计算数组的元素类型
8.3.3 分配率
// extends操作符是用分配率来执行的,以保持输入时的并集
// WithOut<number | string,string> = WithOut<number,string> | WithOut<string ,string>
type WithOut<T, U> = T extends U ? never : T
function testTypeCheck3() {
//推导为number
type R1 = WithOut<number | string, string>
//推导为number | string
type R2 = WithOut<number | object | string, string>
}function testTypeCheck() {
testTypeCheck1()
testTypeCheck2()
}
extends的执行,就像是乘法分配率一样
8.4 类型强制转换
function testTypeConvert() {
// 强行将number类型看成是string类型,这种看成是编译时的,仅仅是为了编译公国而已
// 运行时没有执行任何的类型转换
const a = 123 as unknown as string
try {
console.log(a.toUpperCase())
catch (e) {
} console.log('出错,类型本来就不是string')
} }
强制类型转换,as操作符,这是一种不安全的做法,尽量避免使用
function testNotNull() {
let a: string | undefined
// ?号简要判断,这是运行时判断,只有a不是undefined的时候,才执行后面的toUpperCase
?.toUpperCase()
a
// !号的是强行判断,仅仅是编译通过而已,没有任何的运行时判断
try {
!.toUpperCase()
acatch (e) {
} console.log('出错,类型本来就是undefined')
} }
强制类型非空,?是可选操作,运行时会检查,可恰当使用。!是强行指定,不安全的做法,尽量避免使用。
8.5 类型扩展
declare global {
interface String {
convertSheep(): string
}
}
String.prototype.convertSheep = function (this: string) {
return '【' + this + '】'
}
export default {}
我们可以为原生类型在prototype上扩展,增加一个新方法。注意declare后面有global关键字。
// 即使没有导入prototype_declaration也没有问题
import './prototype_declaration'
function testPrototype() {
const a = '123'
const b = a.convertSheep()
console.log(b)
}
export default testPrototype
即使没有显式导入,ts也能识别到string有这个方法。但要注意,你总得在程序的入口处import这个prototype_declaration,否则运行时找不到convertSheep的实现
8.6 类型推导收窄
function testTypeNarrow1(){
//推导出来的是数组,(number|string)[]
let a = [1,"23"]
console.log(a)
}
function testTypeNarrow2(){
//推导出来的是元组,不过是readonly的。[number,string]
let a = [1,"23"] as const
console.log(a)
}
//使用模板进行类型推导时,类型会尽可能收窄
//T用不定参数的方式传入
function tunple<T extends unknown[]>(...ts:T):T{
return ts
}
function testTypeNarrow3(){
//推导出来的是元组,不是readonly的,[number,string]
let a = tunple(1,"23")
console.log(a)
}
export default function testTypeNarrow(){
testTypeNarrow1()
testTypeNarrow2()
testTypeNarrow3()
}
let声明的类型,会被类型扩宽。用const声明的类型,会进行类型收窄。使用模板推导的类型,会进行类型收窄。
9 集合与映射类型
代码在这里
9.1 对象模拟map
type User = {
: number
id: string
name
}type UserMap = {
in number]: User
[key
}
function TestObjectMockMap1() {
const data: UserMap = {}
// key指定了为number类型,以下这句报错
/*
data.fish = {
id: 1,
name: 'fish',
}
*/
console.log('object mock test....')
1] = {
data[: 1,
id: 'fish',
name
}
2] = {
data[: 2,
id: 'cat',
name
}
console.log(data[1])
console.log(data.hasOwnProperty(1))
console.log(data.hasOwnProperty(12))
}
/*
//你不用创建一个以对象为key的map
type UserMap2 = {
[key in User]: number
}
*/
function TestObjectMockMap2() {
console.log('nothing happen')
}
export default function TestObjectMockMap() {
TestObjectMockMap1()
TestObjectMockMap2()
}
使用key in number或者key in string的方式,我们可以模拟一个map,但是这样做并不安全,因为Object类型本身有prototype,对于prototype上的属性,hasOwnProperty会返回false。
9.2 Set
function TestSet1() {
console.log('set test1....')
const a = new Set<number>()
.add(1)
a.add(2)
aconsole.log(a.has(1)) // true
console.log(a.has(12)) // false
}
type Country = {
: string
name
}
function TestSet2() {
console.log('set test2....')
const data = new Set<Country>()
.add({
data: 'CHINA',
name
}).add({
data: 'CHINA',
name
})
// size为2,不是1,因为js的Set是以对象引用放入的,进行的是浅比较
console.log(data.size)
const data2 = new Set<Country>()
const country = {
: 'US',
name
}.add(country)
data2.add(country)
data2
// size为1
console.log(data2.size)
}export default function TestSet() {
TestSet1()
TestSet2()
}
使用ES6的Set类型更为安全,注意,当key类型是对象的时候,它是用引用比较,而不是值比较,这点跟golang不同。当然,为了代码的可读性,我们一般都尽量避免这样做,只对key使用 number与string这种基础类型
9.3 Map
type Car = {
: string
name: string
brand
}
function TestMap1() {
console.log('map test1....')
const a = new Map<number, Car>()
.set(1, {
a: 'S600',
name: 'BENZ',
brand
}).set(2, {
a: 'RAV4',
name: 'TOYOTA',
brand
})console.log(a.get(1)) // 返回S600
console.log(a.get(12)) // 没有的话,返回undefined
console.log(a.has(1)) // true
console.log(a.has(12)) // false
}
function TestMap2() {
console.log('map test2....')
const a = new Map<Car, number>()
.set(
a
{: 'S600',
name: 'BENZ',
brand,
}1
).set(
a
{: 'S600',
name: 'BENZ',
brand,
}1
)// size为2,也是一样,浅比较
console.log(a.size)
const b = new Map<Car, number>()
const car = {
: 'RAV4',
name: 'TOYOTA',
brand
}.set(car, 1)
b.set(car, 2)
b// size为1,浅比较
console.log(b.size)
}
export default function TestMap() {
TestMap1()
TestMap2()
}
使用ES6的Map类型也不错,这个用法与Set很相似了,没啥好说的
10 坑
10.1 关键字命名
代码在这里
import styles from './index.less';
function Creator(){
const String = (props)=>{
return (<div>{"String"}</div>);
}
const Number = (props)=>{
return (<div>{"Number"}</div>);
}
//这是因为生成代码中,也用到了原生的Object对象,而本地定义的变量也用Object命名,撞在一起了
//解决方法很简单,永远不要以Object,Array,String,Number这些作为变量命名了,真的太傻了
/*
var Object = props => {
return Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__["jsxDEV"])("div", {
children: "Object"
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 11,
columnNumber: 13
}, this);
};
*/
const Object = (props)=>{
return (<div>{"Object"}</div>);
}
const Array = (props)=>{
return (<div>{"Array"}</div>);
}
return {
String:String,
Number:Number,
Object:Object,
Array:Array,
};
}
//以下的代码编译时没有问题,但是运行时会报出Maximum call stack size exceeded错误
const creator = Creator();
export default function IndexPage() {
return (
<div>
<h1 className={styles.title}>Page index</h1>
<div>
<creator.Array/>
<creator.Object/>
<creator.String/>
<creator.Number/>
</div>
</div>
);
}
永远不要以Object,Array,String,Number这些作为变量命名了,真的太傻了
11 总结
既要保持js语言的动态灵活性,又要有编译时的类型检查。TypeScript可以说是带着手铐在跳舞,因此它开启了类型的编译时运算系统,相当有意思。
更多的Demo看这里
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!