8 月 20 日,TypeScript 4.0 即将发布(Announcing TypeScript 4.0)。 即使没有大的改动和功能typescript 语法分析,也可以算是3.9版本的正常迭代。 不过,丹尼尔在公告中也表示:对于初学者来说,今天是最好的入门时机。
事实上,如果您不熟悉该语言,现在是开始使用它的最佳时机。
确实,TS经过几年的发展,越来越多的团队使用TS。 更重要的是,TS的生态越来越完善。 很多库和框架都支持类型系统,甚至直接使用TS来复用Draw,既然开始使用TS,就可以直接享受到整个技术生态带来的开发效率的提升。 回归到业务上,我们团队最近确实开始使用TS进行通用开发,所以我们结合官方文档和社区文档,从新人的角度整理了一份包含TS大部分核心概念的学习指南。 这篇文章主要是加法,整理出核心点,大家可以先用一下,然后再根据工作的需要,找到一些点来一一深入研究。
背景
由于日常工作以后会使用页面构建系统创建很多后端页面,所以会开发很多楼层模块来配合构建系统。 最近基于Rax开发该模块,未来将支持越来越多的投放渠道:web、weex、淘宝小程序、支付宝小程序等,为了兼容越来越多的渠道,很多功能都有被具体化为小型解释器,在泛型中兼容各种渠道,然后在模块中尽可能保留业务代码,只保留清晰的业务逻辑。
并且随着支持渠道的增多,难免出现不同渠道支持的泛型类型特性不一致的情况。 例如,在web中,泛型类型A支持A、B、C三个参数,而在小程序中,泛型类型A只支持A、B两个参数,所以参数C应该设计为可选参数。 在类似场景越来越多之后,这种通用文档就成为一项非常重要的任务。 同时,如果在IDE中编写代码的时候有类型的话,如果系统能够手动告诉开发者这个函数支持哪些参数就更好了,所以我们打算重画通用的TS来提高我们的生产效率,同时也为后续在团队中实施TS打下了良好的基础。
什么是 TypeScript
先说一下目前JS的情况:
JS中变量本身没有类型,变量可以接受任意不同类型的值,同时可以访问任意属性。 如果该属性不存在,无非是返回undefined
JS也是有类型的,JS的类型是和值绑定的,也就是值的类型。 虽然变量类型是由typeof决定的,但它是当前值的类型
// JavaScript
var a = 123
typeof a // "number"
a = 'sdf'
typeof a // "string"
a = { name: 'Tom' }
a = function () {
return true
}
a.xxx // undefined
TS所做的就是给变量添加类型限制
当限制为变量参数时,必须提供类型匹配的值。
限制变量仅访问它们所绑定的类型中存在的属性和技术
举个简单的例子,下面是一段可以正常执行的JS代码:
let a = 100
if (a.length !== undefined) {
console.log(a.length)
} else {
console.log('no length')
}
直接用TS重画里面的代码,将变量a的类型设置为number
TS中设置变量类型的句型是[:Type]类型注释
let a: number = 100
if (a.length !== undefined) { // error TS2339: Property 'length' does not exist on type 'number'.
console.log(a.length)
} else {
console.log('no length')
}
而如果直接编译这段TS代码,就会报错,因为当变量被限制为某个类型时,很难访问该类型中不存在的属性或者技巧。
然后写一个可以正常执行的TS
let a: string = 'hello'
console.log(a.length)
编译成JS的代码是
var a = 'hello'
console.log(a.length)
可以发现编译后不存在string的类型限制,只在编译时进行类型校准。
当TS源码最终编译成JS时,不会形成类型代码,所以运行时自然不会进行类型校准。
也就是说假设一个项目是用TS写的,加上各种类型检查,项目测试部署到线上然后
最终客户端运行的代码和我直接用JS写的代码是一样的。 我额外写了很多类型代码,以确保它能成功编译成原始代码。
▐TypeScript 的作用
TS有哪些作用,主要有以下三点:
将类型系统视为文档更适合代码结构相对复杂的场景。 这本质上是一个很好的评论。
有了IDE,就有了更好的手动代码补全功能。
通过IDE,可以在代码编译过程中进行一些代码校准。 比如一些if内部类型错误,JS需要执行相应的代码来检测错误,而TS可以在编写代码的过程中检测到一些错误,并且代码交付的质量比较高,但是对于逻辑错误,TS其实是很难辨别的。
TypeScript 类型梳理
TS的类型系统分两类介绍:
如何在TS中限制JS中现有值类型对应的变量
TS中扩展的类型,这种类型也只存在于编译时,虽然编译时和运行时赋值的也是JS已有的值类型
下面会穿插一些像[xx]这样的标题,这些标题是在列出和介绍TS类型的过程中插入的TS概念
▐如何将JS中已有的值类型绑定到变量上
布尔值
let isDone: boolean = false
价值
let age: number = 18
细绳
let name: string = 'jiangmo'
空值
function alertName(): void { // 用 : void 来表示函数没有返回值
alert('My name is Tom')
}
空和未定义
let u: undefined = undefined
let n: null = null
// 注意:和所有静态类型的语言一样,TS 中不同类型的变量也无法相互赋值
age = isDone // error TS2322: Type 'false' is not assignable to type 'number'.
// 但是因为 undefined 和 null 是所有类型的子类型,所以可以赋值给任意类型的变量
age = n // ok
[类型推断]
例如:定义变量时,同时添加了形参,那么TS会在没有类型注解的情况下手动推导变量类型
let age = 18
// 等价于
let age: number = 18
// 所以上面代码中的类型声明其实都可以省略
// 但是如果定义的时候没有赋值,不管之后有没有赋值,则这个变量完全不会被类型检查(被推断成了 any 类型)
let x
x = 'seven'
x = 7
// 所以这个时候应该显示的声明类型
let x: number
x = 7
继续列出类型
字段类型
let nameList: string[] = ['Tom', 'Jerry']
let ageList: number[] = [5, 6, 20]
对象类型
interface Person { // 自定义的类型名称,一般首字母大写
name: string
age: number
}
let tom: Person = {
name: 'Tom',
age: 25,
}
函数类型
// JavaScript
const sum = function (x, y) {
return x + y
}
TS中有多种句型来定义函数类型
const sum = function (x: number, y: number): number {
return x + y
}
const sum: (x: number, y: number) => number = function (x, y) {
return x + y
}
这里,如果直接将函数类型提取出来作为自定义类型名,代码会更加美观,并且易于复用。
TS 类型可以借助类型别名进行重命名
[输入别名]
type MySum = (x: number, y: number) => number
const sum: MySum = function (x, y) {
return x + y
}
返回函数类型
使用套接字定义函数类型
interface MySum {
(a: number, b: number): number
}
const sum: MySum = function (x, y) {
return x + y
}
既然函数类型介绍完了,我补充一下函数类型如何定义其余参数的类型以及如何设置默认参数。
const sum = function (x: number = 1, y: number = 2, ...args: number[]): number {
return x + y
}
班级类型
class Animal {
name: string // 这一行表示声明实例属性 name
constructor(name: string) {
this.name = name
}
sayHi(): string {
return `My name is ${this.name}`
}
}
let a: Animal = new Animal('Jack') // : Animal 约束了变量 a 必须是 Animal 类的实例
console.log(a.sayHi()) // My name is Jack
对了,值得一提的是,除了类型支持之外,TS 还扩展了类的句型特征
添加了三个新的访问修饰符 public、private、protected 和只读属性关键字 readonly 和抽象具体类
这里就不展开了,有需要的话查看官方文档即可
外部对象和外部方法
JavaScript中有很多外部对象和工具函数,TS自带了对应的类型定义
let e: Error = new Error('Error occurred')
let d: Date = new Date()
let r: RegExp = /[a-z]/
let body: HTMLElement = document.body
Math.pow(2, '3') // error TS2345: Argument of type '"3"' is not assignable to parameter of type 'number'.
▐TS中的扩展类型
任何值 任何
与其说any是JS中不存在的类型,不如说JS中变量只有一种类型,就是any
任意值any的特征:
任何类型的变量都可以形式参数化为任何其他类型,这与 null 和 undefined 相同 任何类型都可以形式参数化为任何类型的变量
允许访问任何值的任何属性
let a: any = 123
a = '123' // ok
let n: number[] = a // ok
a.foo && a.foo() // ok
所以any是万能的,也违背了TS的类型约束的目的。 尽量避免使用任何。
联合型
let x: string | number = 1
x = '1'
然而,联合类型有一个额外的约束:
当 TypeScript 不确定联合类型的变量是什么类型时,我们只能访问该联合类型的所有类型通用的属性或技术。
let x: string | number = 1
x = '1'
x.length // 这里能访问到 length ,因为 TS 能确定此时 x 是 string 类型
// 下面这个例子就会报错
function getLength(something: string | number): number {
return something.length // error TS2339: Property 'length' does not exist on type 'string | number'.
}
两种解决方案
function getLength(something: string | number): number {
if (typeof something === 'string') { // TS 能识别 typeof 语句
return something.length // 所以在这个 if 分支里, something 的类型被推断为 string
} else {
return 0
}
}
使用类型断言自动强制现有类型
function getLength(something: string | number): number {
return (something as string).length // 不过这样做实际上代码是有问题的,所以用断言的时候要小心
}
[类型判定]
使用类型断言更改类型时的限制:
联合类型可以断言为其中一种类型
父类可以被断言为泛型
任何类型都可以断言为任何
any 可以被谓词为任何类型
总有一个规则可以得出:要促使A被判定为B,只有A与B兼容或B与A兼容。
✎双重判断
let a = 3
(a as any) as string).split // ok
如果说判断有风险,那么双重判断就是重复跳跃
字符串文字类型
type EventNames = 'click' | 'scroll' | 'mousemove'
function handleEvent(ele: Element, event: EventNames) {
// do something
}
请注意,只有一个字符串也是字符串文字类型
type MyType = 'hello'
尽管这样的类型通常不会自动设置,但它通常是通过类型推断来推断的。
例如,编译错误消息为:Argumentoftype'"foo"'isnotassignabletoparameteroftype'number'。
提示中的类型“foo”通常是从字符串“foo”推导出来的字符串文字类型。
元组
let man: [string, number] = ['Tom', 25]
// 不过 TS 中的元组支持越界
// 当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型
man.push('male')
枚举
enum Directions {Up,
Down,
Left,
Right,
}
let d: Directions = Directions.Left
这里听说Directions.Left直接使用类型作为值。
不是说[:Type]类型注释句型中使用type来约束变量,编译后类型代码会被删除吗?
为了解释这一点,我们首先看看普通类型代码被编译成什么。
type MyType = string | number | boolean
编译结果:
// 不会产生任何 JS 代码
enum Directions {
Up,
Down,
Left,
Right,
}
console.log(Directions)
编译结果:
var Directions
;(function (Directions) {
Directions[(Directions['Up'] = 0)] = 'Up'
Directions[(Directions['Down'] = 1)] = 'Down'
Directions[(Directions['Left'] = 2)] = 'Left'
Directions[(Directions['Right'] = 3)] = 'Right'
})(Directions || (Directions = {}))
console.log(Directions)
/*
运行时 log 出来的 Directions 变量如下
{
'0': 'Up',
'1': 'Down',
'2': 'Left',
'3': 'Right',
Up: 0,
Down: 1,
Left: 2,
Right: 3
}
*/
这怎么理解呢?
let d: Directions = Directions.Left
虽然在这行代码中,前面的Directions代表类型,后面的Directions代表值。
即Directions是值和类型的“复合体”,在不同的句型中被抽象为值或类型。
尽管有多种方法可以从方向中提取类型部分。
enum Directions {
Up,
Down,
Left,
Right,
}
type MyDirections = Directions
console.log(MyDirections) // error TS2693: 'MyDirections' only refers to a type, but is being used as a value here.
此时,MyDirections 是纯类型typescript 语法分析,不能用作值。
虽然在之前介绍的函数类型、类类型等声明中,也存在着这样的值和类型的“复合体”。
const sum = function (x: number, y: number = 5): number {
return x + y
}
console.log(sum) // [Function: sum]
type MySum = typeof sum // 注意,剥离出来的函数类型是不会带有默认参数的,因为默认参数其实是函数的特性,和类型系统无关
const f: MySum = (a, b) => 3 // ok
console.log(MySum) // error TS2693: 'MySum' only refers to a type, but is being used as a value here.
稍后再回到枚举。
✎字符串枚举
enum Directions {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
console.log(Directions.Up === 'UP') // true
✎常量枚举
const enum Directions {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
let d = Directions.Left
// 如果取消注释下面这行代码,编译会报错
// console.log(Directions) // error TS2475: 'const' enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query.
编译结果:
var d = 'LEFT' /* Left */
类库
假设一个场景,函数的输入参数类型为数字|字符串,但输出参数类型与输入参数相同。
首先尝试使用union类型来约束输入输出参数
type MyFunc = (x: number | string) => number | string
但MyFunc很难表明输出参数类型与输入参数相同,即当输入参数为数字时,输出参数也是数字。
在这种情况下,您可以使用类库来定义多个相似的函数类型。
库函数
function GenericFunc<T>(arg: T): T {
return arg
}
// 这里的 GenericFunc 是表示的是一个函数值,同时将类型参数 T 赋值为 number
let n = GenericFunc<number>(1) // n 可以通过类型推论得出类型为 :number
// 进一步,利用 泛型约束 ,限制出入参为 number | string
type MyType = number | string
function GenericFunc<T extends MyType>(arg: T): T { // extends MyType 表示类型参数 T 符合 MyType 类型定义的形状
return arg
}
let s = GenericFunc<string>('qq')
let b = GenericFunc<boolean>(false) // error TS2344: Type 'boolean' does not satisfy the constraint 'string | number'.
类库套接字
interface GenericFn {
(arg: T): T
}
// 定义一个泛型函数作为函数实现
function identity<T>(arg: T): T {
return arg
}
// 使用泛型时传入一个类型来使 类型参数 变成具体的类型
// 表示 T 此时就是 number 类型,GenericFn 类似是 “函数调用” 并返回了一个具体的类型 (这里是一个函数类型)
const myNumberFn: GenericFn<number> = identity
const myStringFn: GenericFn<string> = identity
let n = myNumberFn(1) // n 可以通过类型推论得出类型为 :number
let s = myStringFn('string') // s 可以通过类型推论得出类型为 :string
对比上面的类库函数和基类socket,有一个区别:
// GenericFunc 是上面定义的泛型函数
type G = GenericFunc<string> // error TS2749: 'GenericFunc' refers to a value, but is being used as a type here.
// GenericFn 是上面定义的泛型接口
type G = GenericFn<number> // ok
GenericFn<number>() // error TS2693: 'GenericFn' only refers to a type, but is being used as a value here.
子类类
class GenericClass {
zeroValue: T
constructor(a: T) {
this.zeroValue = a
}
}
let instance = new GenericClass<number>(1)
// 等价于
let instance: GenericClass<number> = new GenericClass(1)
// 因为有类型推论,所以可以简写成
let instance = new GenericClass(1)
✎外部字段子类
// 数组的类型之前是用 【 Type[] 】 语法来表示的
let list: number[] = [1, 2, 3]
// 现在也可以这么表示
let list: Array<number> = [1, 2, 3]