8 月 20 日,TypeScript 4.0 正式发布(Announcing TypeScript 4.0)。 虽然没有大的改动和功能,但也算是3.9版本的正常迭代。 不过,大牛在公告中也表示:对于初学者来说,现在是最好的入门时机。
事实上,如果您不熟悉该语言,现在是开始使用它的最佳时机。
确实,TS经过几年的发展,越来越多的团队使用TS。 更重要的是,TS的生态越来越完善。 很多库、框架等都支持类型系统,甚至直接使用TS来复用,如果现在开始使用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来确定变量类型似乎是确定当前值的类型。
<pre class="code-snippet__js" data-lang="javascript">// JavaScript
var a = 123
typeof a // "number"
a = 'sdf'
typeof a // "string"
a = { name: 'Tom' }
a = function () {
return true
}
a.xxx // undefined
TS所做的就是给变量添加类型限制
当限制为变量参数时typescript对象赋值,必须提供类型匹配的值。
限制变量仅访问它们所绑定的类型中存在的属性和技术
举个简单的例子,下面是一段可以正常执行的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)
可以发现编译后不存在字符串类型限制,只在编译时进行类型校准。
当TS源码最终编译成JS时,不会形成类型代码,所以运行时自然不会进行类型校准。
换句话说,假设一个项目是用TS编写的,并且添加了各种类型的测试。 项目测试部署上线后,
最终客户端运行的代码和我直接用JS写的代码是一样的。 我额外写了很多类型代码,以确保它能成功编译成原始代码。
TypeScript 的作用
那么TS有哪些作用呢,主要有以下三点:
将类型系统视为文档更适合代码结构相对复杂的场景。 这本质上是一个很好的评论。
有了IDE,就有了更好的手动代码补全功能。
通过IDE,可以在代码编译过程中进行一些代码校准。 比如一些if内部类型错误,JS需要执行相应的代码来检测错误,而TS可以在编写代码的过程中检测到一些错误,并且代码交付的质量比较高,但是当然TS也是如此难以识别的逻辑错误。
TypeScript 类型梳理
TS的类型系统分两类介绍:
JS 中现有的值类型如何对应 TS 中的限制变量?
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
对了typescript对象赋值,值得一提的是,除了类型支持之外,TS 还扩展了类的句型特征
添加了三个新的访问修饰符public、private、protected和只读属性关键字readonly和abstract抽象类
这里就不展开了,有需要的话查看官方文档即可
内置对象和外部方法
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'
尽管此类类型通常不会自动设置,但它们通常是通过类型推断来推断的。
例如,编译错误消息为:“Argument of type '"foo"' is not assignable to argument of type '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 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是值和类型的“复合体”,在不同的句型中被抽象为值或类型。
事实上,有一种方法可以从 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是纯类型,不能用作值。
其实,在之前介绍的函数类型和类类型的声明中,也存在着这样的值和类型的“复合体”。
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 */
通用的
假设一个场景,函数的输入参数类型为number | string,输出参数类型与输入参数相同。
首先尝试使用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
对比上述通用函数和通用套接字,有一个区别:
// 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]