我正在参加「掘金·启航计划」
前言
以下代码案例只是为了方便书写故有的地方存在使用 const 多次声明。
datatype
JS:
- null、undefined
- string、number、boolean
- bigint、symbol
- object(含 Function、Array、Date...)
TS:
- 以上所有
- 加上 void、never、enum、unknown、any
- 再加上自定义类型 type、interface
从集合的角度理解 TS 数据类型
JS 的类型:每一个值(数据)的类型
TS 的类型:一类数据的类型
JS 类型
null、undefined、1、2、3、4、5、a、b、c、[]、function(){}、new Date()...
TS 类型
- 所有的数字包含在一起取一个名字叫 number
- type number = 1 | 2 | 3 | 4 | 5 | 6 | 6.2 | 7|...
- 所有的只有看起来像字符串的统一起来命名为 string
- type string = a | b | c | d | fff | xxx | ...
- type boolean = ture | false
- type Object = 普通对象 | Array | Function | String | Number | Boolean | RegExp ...
- 小写的 object 就是正常所理解的 object, 比如 数组、函数、对象 (小写 object 一般用的少)
- ⚠️注意:这里的 | 读做 并
TS 为什么有两 string number boolean ?
const n = 9
const m = new Number(9)
它们的区别是:
当写 n = 9 它就是 9 (内存里面就是 0、1 存了 64 位)
当写 new Number(9) 的时候,会封装一个对象
{
constructor: ...
toFixed: ...
toString: ...
valueOf: function(){
return 9
}
}
当我 m.valueOf() 的时候才会返回一个真正的数字 9
包装对象
6..toFixed(2) // '6.00'
为何 6 有一个 toFixed 方法, 6 不是由 0 和 1 表示的?那为何会有一个 toFixed 方法呢?
因为 JS 有一个包装对象的暗箱操作。
当它发现 6 没有 toFixed 的时候,它就会把变成 new Number(6)
,并删除旧的 6。
# 大概过程
let temp = new Number(6)
value = temp.toFixed(2)
删除 temp
value
总结
JS 不会直接使用 new Number(),它会在操作一个 number 的时候,由 浏览器/node 包装出一个 new Number() 来使用(永远都是暗箱操作的使用)。
JS 中的 Number、String、Boolean 只用于包装对象,正常开发者不用它们,在 TS 里也不用。
也就是说当我们在写 TS 的时候,永远都要写 小写的 number、string、boolean,不要写大写的。
用类型签名和 Record 描述对象
type Object 表示的范围太大了
const a: Object = []
const b: Object = () => 1
const c: Object = /ab+c/
以上代码都不会报错,没有发挥出 TS 用来描述这个对象有哪些特性的功能,所以不用Object 。
如何在 TS 里描述对象的数据类型
- 用 class/constructor 描述
- 用 type 或 interface 描述
// 在 TS 中以下有三种写法
type Person = {name: string; age: number}
type Person = {name: string, age: number}
type Person = {
name: string
age: number
}
// JS 中 就不可以写成以上的三种写法
const a:Person = {
name: 'hone',
age: 18
}
// 以上代码少写/多些、类型不对都会报错、对象字面量只能指定知道的属性
// 经常会用到的一种索引签名的用法
// 对象的下标 k 必须是一个 string, 对象的 value 必须是一个 number
type A = {
[k: string]: number
}
// 另一种写法
type A2 = Record<string, number>
// 以上 A 和 A2 所表达的含义是一摸一样的
// 还可以写的具体一点
type A3 = {
name: string
age: number
}
// 用法
const a: A = {
name: 1,
123: 6
}
// 在 JS 层面来说 123 最终会变成字符串
type A = {
[k: symbol]: number
}
// JS 中 如果 symbol 作为 key 必须用🀄括号包起来
const a: A = {
[s]: 1
}
key 的类型可以不是 string, 可以是 number、symbol。
结论
由于 object 太不精确,所以 TS 开发者一般使用,索引签名 或 Record 泛型 来描述普通对象
用 [] 和 Array 泛型来描述数组对象
数组对象该怎么描述
type A = string[]
const a: A = ['h', 'i']
type B = number[]
const b: B = [1, 2, 3]
// 以上写法和下面等价
type A = Array<string>
type B = Array<number>
type D = [string, string, string]
const noError: D = ['加', '油', '冲']
const error: D = ['h', 'i'] // error: source has 2 elements but target requires 3
type E = [string, number]
const e:E = ['老李', 11]
type F = [string[], number[]]
const f: F = [['柴', '米', '油', '盐'], [1, 2, 3]]
const Two = [number, number] // 二元组
const Three = [number, number, number] // 三元组
二元组里面不能有三个元素,三元组里面不能有两个元素
结论
由于 Array 太不精确,所以 TS 开发者一般用 Array<?>
或 string[]
或 [string, number]
, 来描述数组
// 只含有一个元素的集合也是集合
// 当你把类型看成集合的时候以下写法是正确的
type A = [1, 2, 3|4]
const a: A = [1, 2, 4]
描述函数对象(这里先只介绍简单写法)
type FnA = (a: number, b: number) => number
// 下面代码不报错,因为 TS 可以认为是松散的类型检查(参数些少了是可以的,能少但不能多)
const a: FnA = () => {
return 123
}
// 类型推导:当我们在前面写了类型 (:FnA), 后面 (x, y) 就可以不用写了
const a: FnA = (x, y) => {
return 123
}
// 当调用的时候,就必须按照类型描述去传,不能多也不能少
a(1, 2)
type FnB = (x: string, y: string) => string
void
// 如果函数没有返回值 就可以写一个 返回值为 void, 不要写 undefined 和 null
type FnReturnVoid = (s: string) => void
// 一般不会写这种
type FnReturnUndefined = (s: string) => undefined
const v: FnReturnVoid = (s: string) => {
console.log(s)
} // 这个的返回值是 void
const u: FnReturnUndefined =(s: string) => {
console.log(s)
return undefined
} // 这里不写 return undefined | null 就会报错,TS 不会自动 ruturn undefined
// 声明一个 支持 this 的普通函数,你依然要使用箭头函数来声明它的类型
type Person = {
name: string
age: number
sayHi: FnWithThis
}
type FnWithThis = (this: Person, name: string) => void
// 2. 你这个函数一般来说不能用箭头函数,只能用 function
const sayHi: FnWithThis = function(){
console.log('hi ' + this.name) // 打印出 hi frank
}
const x: Person = {
name: 'frank',
age: 18,
sayHi: sayHi
}
// 1. 如果你的函数声明有 this ,你在调用这个函数的时候,你必须显示的传递 this
x.sayHi('hone')
// sayHi.call(x, 'hone')
结论
由于 Function太不精确,所以 TS 开发者一般用 () => ?
来描述 函数
描述其他对象
其他对象一般直接用 class 描述
type A = object
const a: A = ()=> 1
const b: A = {}
const c: A = []
const d: Date = new Date()
const r: RegExp = /ab+c/
const r2: RegExp = new RegExp('ab+c')
const m: Map<string, number> = new Map()
m.set("xxx", 2)
const wm: WeakMap<{name: string}, number> = new WeakMap()
const s: Set<number> = new Set()
s.add(123)
const ws: WeakSet<string[]> = new Weakset()
const n: bigint = 100n // >=es2020
const s: symbol = Symbol()
// DOM
const button = document.getElementById("xxx")
if(button) { button } // 这里就不为 null
// null 的集合只有 null
type A = null
const a: A = null
// undefined 的集合只有 undefined
type A = undefined
const a: A = undefined
any 和 unknown
any
如果 a: number | string
那么 a 可以等于 1 也可以等于 "x"。
如果范围再扩大一点 a: number | string | boolean
再大 a: number | string | boolean | {}
... 一直大到把所有的都包起来
那就可以用 any
表示, any 可以说是全知全能的类型。
unknown
unknowm 就是我有一个圈,我随便画在哪里,这个圈的大小不知道,圈的直径、范围不知道,随便丢进去,框到哪个算哪个。
const a: unknown = 1
// 这里不可以 .toFixed 因为它的类型是 unknown
// 虽然我在开始声明 a 的时候我告诉你是一个 1,但是我把它盖住了(被圈盖住了),你不能使用
// 不写 unknown 就可以使用以下 toFixed 方法
a.toFixed()
所以 unknown 就是我把这个类型盖住我不让你知道。
那为何要盖住呢?并不是故意要盖住的,因为很有可能这个 1 是我从远端得到的
// 有可能是从 ajax 得到的一个 1, 从一开始确实不知道是什么
// 我先盖这个地方,然后我从网络上来个一个值,放在这个盖的下面
const a: unknown = await ajax.get('/api/users')
// 一开始我不知道什么类型,你现在告诉我(你取的数据你知道类型)
;(a as number).toFixed()
所以 unknown 特别适合 你这个值是从外部获取的,没有办法提前知道 那我就盖住,你想用的时候,再翻起来,使用 as 断言
总结
any: 我什么都要,如果用 any 就没有机会去断言,想怎么写就怎么写,不会报错,全集,少了一次检查的机会。
unknown: 我先盖住,等要用的时候自己再去归一类(玩骰子猜大小),如果接错了就自己负责,未知集,一般说推荐使用 unknown,这样我就有一个机会去断言。
const b: unknown = 1;
(b as string).split(',') // 自己断言的出错就自己负责
never
never 里面什么都没有,空集,一个不包含任何元素的集合叫做 never。
空集用来做检查
type A= string & number // A 是 never
// 这个 never 不是用来声明的,而是用来推断的
type B = string | number | boolean
const a: B = ('hi' as any)
if (typeof a === 'string') {
a.split('')
} if (typeof a === 'number') {
a.toFixed(2)
} if (typeof a === 'boolean') {
a.valueOf()
} else {
console.log('never')
}