本文知识图谱分基础、进阶和实战三个部分,分别如下:
image.png
image.png
image.png
这三份知识图谱里罗列的知识看似很多,实际上除了进阶部分的泛型、高级类型(二)和实战部分需要稍微费点脑细胞外,其他的都很好理解。
本文罗列的知识点比官方文档精简,但基本能覆盖日常开发需要用的 TS 知识,学完之后应付开发完全没问题。
为什么要学 TS
这是趋势,2022 年了,TS 已经快成为一个前端的基本技能了。
就跟几年前,jquery 时代快结束时,你不会 vue 或者 react一样,根本找不到工作。
如今别说 React,连 Vue 的默认版本都已经是 Vue3 了,Vue3 和 Typescript 是绑在一起的,再过两年,除了祖传老项目,新项目就是清一色的 TS 了。
如何学习本文
不用去考虑配环境的问题,直接去这个在线地址 TS Playground[3] ,把本文的例子都写一遍,一定要亲自手写。
学习本文的时候,不用从头到尾地去看,因为概念看多了很容易倦,可以收藏起来,每天摸鱼时看一两个概念,一两周就看完了。
阿林也是每天等后端联调、等测试验收时摸鱼学习的 TS,一天学一两个概念,整理完本文刚好花了两周。
你可以把本文当作最最最通俗基础的 TS 入门文章,看官方文档看不懂时来本文对比学习一下,也许就能理解官方文档中一些难懂的概念了,本文的所有知识只适合初学者。
另外,别担心以后不会配置环境,工程化的问题肯定有一大堆脚手架支持的,比如(vue-cli、vite),把语法学会才是精髓。
现在就动手吧!
TS 与 JS 的区别
TypeScript[4] 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
说人话就是 TS 拓展了 JS 的一些功能,解决了 JS 的一些缺点,可以总结在下面的表格里,
TypeScript | JavaScript |
---|---|
JavaScript 的超集,用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页。 |
强类型,支持静态和动态类型 | 动态弱类型语言 |
可以在编译期间发现并纠正错误 | 只能在运行时发现错误 |
不允许改变变量的数据类型 | 变量可以被赋予不同类型的值 |
关于强类型、弱类型、静态类型和动态类型语言,可以看我的这篇文章[5]。
用一张图来描述一下 TS 和 JS 的关系,
image.png
JS 有的, TS 都有, JS 没有的, TS 也有,毕竟 TS 是 JS 的超集嘛。
TS 的缺点:
-
不能被浏览器理解,需要被编译成 JS
-
有学习成本,写习惯了 JS 的我们要上手需要花时间去理解,而且 TS 中有一些概念还是有点难,比如泛型。
TS 基础
这一部分的内容是比较简单的,有 JS 基础的同学把例子写一遍就理解了。
基础类型
boolean、number 和 string 类型
boolean
let isHandsome: boolean = true
复制代码
赋值与定义的不一致,会报错,静态类型语言的优势就体现出来了,可以帮助我们提前发现代码中的错误。
let age: number = 18
复制代码
string
let realName: string = 'lin'
let fullName: string = `A ${realName}` // 支持模板字符串
复制代码
undefined 和 null 类型
let u:undefined = undefined // undefined 类型
let n:null = null // null 类型
复制代码
默认情况下 null
和 undefined
是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。
let age: number = null
let realName: string = undefined
复制代码
但是如果指定了 --strictNullChecks
标记,null 和 undefined 只能赋值给 void 和它们各自,不然会报错。
image.png
any、unknown 和 void 类型
any
不清楚用什么类型,可以使用 any 类型。这些值可能来自于动态的内容,比如来自用户输入或第三方代码库
let notSure: any = 4
notSure = "maybe a string" // 可以是 string 类型
notSure = false // 也可以是 boolean 类型
notSure.name // 可以随便调用属性和方法
notSure.getName()
复制代码
不建议使用 any,不然就丧失了 TS 的意义。
unknown 类型
不建议使用 any,当我不知道一个类型具体是什么时,该怎么办?
可以使用 unknown
类型
unknown
类型代表任何类型,它的定义和 any
定义很像,但是它是一个安全类型,使用 unknown
做任何事情都是不合法的。
比如,这样一个 divide 函数,
function divide(param: any) {
return param / 2;
}
复制代码
把 param 定义为 any 类型,TS 就能编译通过,没有把潜在的风险暴露出来,万一传的不是 number 类型,不就没有达到预期了吗。
把 param 定义为 unknown 类型 ,TS 编译器就能拦住潜在风险,如下图,
image.png
因为不知道 param 的类型,使用运算符 /
,导致报错。
再配合类型断言,即可解决这个问题,
function divide(param: unknown) {
return param as number / 2;
}
复制代码
void
void
类型与 any
类型相反,它表示没有任何类型。
比如函数没有明确返回值,默认返回 Void 类型
function welcome(): void {
console.log('hello')
}
复制代码
never 类型
never
类型表示的是那些永不存在的值的类型。
有些情况下值会永不存在,比如,
-
如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接中断程序运行。
-
函数中执行无限循环的代码,使得程序永远无法运行到函数返回值那一步。
// 异常
function fn(msg: string): never {
throw new Error(msg)
}
// 死循环 千万别这么写,会内存溢出
function fn(): never {
while (true) {}
}
复制代码
never 类型是任何类型的子类型,也可以赋值给任何类型。
没有类型是 never 的子类型,没有类型可以赋值给 never 类型(除了 never 本身之外)。即使 any
也不可以赋值给 never 。
let test1: never;
test1 = 'lin' // 报错,Type 'string' is not assignable to type 'never'
复制代码
let test1: never;
let test2: any;
test1 = test2 // 报错,Type 'any' is not assignable to type 'never'
复制代码
数组类型
let list: number[] = [1, 2, 3]
list.push(4) // 可以调用数组上的方法
复制代码
数组里的项写错类型会报错
image.png
push 时类型对不上会报错
image.png
如果数组想每一项放入不同数据怎么办?用元组类型
元组类型
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
let tuple: [number, string] = [18, 'lin']
复制代码
写错类型会报错:
image.png
越界会报错:
image.png
可以对元组使用数组的方法,比如使用 push 时,不会有越界报错
let tuple: [number, string] = [18, 'lin']
tuple.push(100) // 但是只能 push 定义的 number 或者 string 类型
复制代码
push 一个没有定义的类型,报错
函数类型
TS 定义函数类型需要定义输入参数类型和输出类型。
输出类型也可以忽略,因为 TS 能够根据返回语句自动推断出返回值类型。
function add(x:number, y:number):number {
return x + y
}
add(1,2)
复制代码
函数没有明确返回值,默认返回 Void 类型
function welcome(): void {
console.log('hello');
}
复制代码
函数表达式写法
let add2 = (x: number, y: number): number => {
return x + y
}
复制代码
可选参数
参数后加个问号,代表这个参数是可选的
function add(x:number, y:number, z?:number):number {
return x + y
}
add(1,2,3)
add(1,2)
复制代码
注意可选参数要放在函数入参的最后面,不然会导致编译错误。
image.png
默认参数
function add(x:number, y:number = 100):number {
return x + y
}
add(100) // 200
复制代码
跟 JS 的写法一样,在入参里定义初始值。
和可选参数不同的是,默认参数可以不放在函数入参的最后面,
function add(x:number = 100, y:number):number {
return x + y
}
add(100)
复制代码
看上面的代码,add 函数只传了一个参数,如果理所当然地觉得 x 有默认值,只传一个就传的是 y 的话,就会报错,
image.png
编译器会判定你只传了 x,没传 y。
如果带默认值的参数不是最后一个参数,用户必须明确的传入 undefined
值来获得默认值。
add(undefined,100) // 200
复制代码
函数赋值
JS 中变量随便赋值没问题,
image.png
但在 TS 中函数不能随便赋值,会报错的,
image.png
也可以用下面这种方式定义一个函数 add3,把 add2 赋值给 add3
let add2 = (x: number, y: number): number => {
return x + y
}
const add3:(x: number, y: number) => number = add2
复制代码
有点像 es6 中的箭头函数,但不是箭头函数,TS 遇到 :
就知道后面的代码是写类型用的。
当然,不用定义 add3 类型直接赋值也可以,TS 会在变量赋值的过程中,自动推断类型,如下图:
image.png
函数重载
函数重载是指两个函数名称相同,但是参数个数或参数类型不同,他的好处显而易见,不需要把相似功能的函数拆分成多个函数名称不同的函数。
不同参数类型
比如我们实现一个 add 函数,如果传入参数都是数字,就返回数字相加,如果传入参数都是字符串,就返回字符串拼接,
function add(x: number[]): number
function add(x: string[]): string
function add(x: any[]): any {
if (typeof x[0] === 'string') {
return x.join()
}
if (typeof x[0] === 'number') {
return x.reduce((acc, cur) => acc + cur)
}
}
复制代码
在 TS 中,实现函数重载,需要多次声明这个函数,前几次是函数定义,列出所有的情况,最后一次是函数实现,需要比较宽泛的类型,比如上面的例子就用到了 any。
不同参数个数
假设这个 add 函数接受更多的参数个数,比如还可以传入一个参数 y,如果传了y,就把 y 也加上或拼接上,就可以这么写,
function add(x: number[]): number
function add(x: string[]): string
function add(x: number[], y: number[]): number
function add(x: string[], y: string[]): string
function add(x: any[], y?: any[]): any {
if (Array.isArray(y) && typeof y[0] === 'number') {
return x.reduce((acc, cur) => acc + cur) + y.reduce((acc, cur) => acc + cur)
}
if (Array.isArray(y) && typeof y[0] === 'string') {
return x.join() + ',' + y.join()
}
if (typeof x[0] === 'string') {
return x.join()
}
if (typeof x[0] === 'number') {
return x.reduce((acc, cur) => acc + cur)
}
}
console.log(add([1,2,3])) // 6
console.log(add(['lin', '18'])) // 'lin,18'
console.log(add([1,2,3], [1,2,3])) // 12
console.log(add(['lin', '18'], ['man', 'handsome'])) // 'lin,18,man,handsome'
复制代码
其实写起来挺麻烦的,后面了解泛型之后写起来会简洁一些,不必太纠结函数重载,知道有这个概念即可,平时一般用泛型来解决类似问题。
interface
基本概念
interface
(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。
定义 interface 一般首字母大写,代码如下:
interface Person {
name: string
age: number
}
const p1: Person = {
name: 'lin',
age: 18
}
复制代码
属性必须和类型定义的时候完全一致。
少写了属性,报错:
image.png
多写了属性,报错:
image.png
类型提示,显著提升开发效率:
image.png
注意:interface 不是 JS 中的关键字,所以 TS 编译成 JS 之后,这些 interface 是不会被转换过去的,都会被删除掉,interface 只是在 TS 中用来做静态检查。
可选属性
跟函数的可选参数是类似的,在属性上加个 ?
,这个属性就是可选的,比如下面的 age 属性
interface Person {
name: string
age?: number
}
const p1: Person = {
name: 'lin',
}
复制代码
只读属性
如果希望某个属性不被改变,可以这么写:
interface Person {
readonly id: number
name: string
age: number
}
复制代码
改变这个只读属性时会报错。
image.png
interface 描述函数类型
interface 也可以用来描述函数类型,代码如下:
interface ISum {
(x:number,y:number):number
}
const add:ISum = (num1, num2) => {
return num1 + num2
}
复制代码
自定义属性(可索引的类型)
上文中,属性必须和类型定义的时候完全一致,如果一个对象上有多个不确定的属性,怎么办?
可以这么写。
interface RandomKey {
[propName: string]: string
}
const obj: RandomKey = {
a: 'hello',
b: 'lin',
c: 'welcome',
}
复制代码
如果把属性名定义为 number 类型,就是一个类数组了,看上去和数组一模一样。
interface LikeArray {
[propName: number]: string
}
const arr: LikeArray = ['hello', 'lin']
arr[0] // 可以使用下标来访问值
复制代码
当然,不是真的数组,数组上的方法它是没有的。
image.png
duck typing(鸭子类型[6])
看到这里,你会发现,interface 的写法非常灵活,它不是教条主义。
用 interface 可以创造一系列自定义的类型。
事实上, interface 还有一个响亮的名称:duck typing
(鸭子类型)。
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
这句话完美地诠释了 interface 的含义,只要数据满足了 interface 定义的类型,TS 就可以编译通过。
举个例子:
interface FunctionWithProps {
(x: number): number
fnName: string
}
复制代码
FunctionWithProps 接口描述了一个函数类型,还向这个函数类型添加了 name 属性,这看上去完全是四不像,但是这个定义是完全可以工作的。
const fn: FunctionWithProps = (x) => {
return x
}
fn.fnName = 'hello world'
复制代码
事实上, React 的 FunctionComponent(函数式组件)
就是这么写的,
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
复制代码
别担心这一大堆看不懂的东西,后面的实战环节会分析的。
现阶段我们只关心 FunctionComponent 是用 interface 描述的函数类型,且向这个函数类型添加了一大堆属性,完全四不像,但是却是完全正常的工作。
这就是 duck typing 和 interface,非常的灵活。
类
我们知道, JS 是靠原型和原型链来实现面向对象编程的,es6 新增了语法糖 class。
TS 通过 public
、private
、protected
三个修饰符来增强了 JS 中的类。
在 TS 中,写法和 JS 差不多,只是要定义一些类型而已,我们通过下面几个例子来复习一下类的封装、继承和多态。
基本写法
定义一个 Person 类,有属性 name 和 方法 speak
class Person {
name: string
constructor(name: string) {
this.name = name
}
speak() {
console.log(`${this.name} is speaking`)
}
}
const p1 = new Person('lin') // 新建实例
p1.name // 访问属性和方法
p1.speak()
复制代码
继承
使用 extends 关键字实现继承,定义一个 Student 类继承自 Person 类。
class Student extends Person {
study() {
console.log(`${this.name} needs study`)
}
}
const s1 = new Student('lin')
s1.study()
复制代码
继承之后,Student 类上的实例可以访问 Person 类上的属性和方法。
image.png
super关键字
注意,上例中 Student 类没有定义自己的属性,可以不写 super ,但是如果 Student 类有自己的属性,就要用到 super 关键字来把父类的属性继承过来。
比如,Student 类新增一个 grade(成绩) 属性,就要这么写:
class Student extends Person {
grade: number
constructor(name: string,gra