why typescript?
JavaScript是一门非常优秀的编程语言,但也存在一些缺点,如JavaScript没有类型检测这一机制。没有类型检测,就会导致我们的代码可能在运行期间或者代码上线后,才会发现错误。然而我们都期望错误出现的越早越好,所以如果增加了类型检测,就能很大程度上帮助我们更早的发现错误,也能让我们的代码更规范、更具可读性。
typescript是拥有类型的JavaScript超集,它可以被编译成普通、干净、完成的JavaScript代码。
JavaScript所拥有的特性,typescript全部都是支持的,而且ts不仅增加了类型约束,也包括了一些语法的扩展。
ts始于js,归于js,即ts最终会被编译成js代码,所以我们并不需要担心它的兼用型问题。
编译环境
ts最终会被编译成JavaScript来运行,这就需要使用编译工具来实现
# 全局安装typescript,内部包含typescript compiler
npm install typescript -g
# 查看版本
tsc --version
# 进行编译
tsc ./hello-world.ts
使用ts-node,更方便我们查看ts代码的运行效果
# 全局安装ts-node
npm install ts-node -g
# 全局安装ts-node依赖的两个包
npm install tslib @types/node -g
# 直接运行ts代码
ts-node ./hello-word.ts
变量声明&数据类型
变量声明格式
var/let/const 标识符: 数据类型 = 赋值;
声明了类型后,ts就会进行类型检测,声明的类型可以称为类型注解
注意:这里的数据类型如string、number、boolean都是小写,表示的是ts中定义的字符串类型,而不是大写(表示的是ECMAScript中定义的一个类)
变量的类型推导
ts本身是可以帮助我们推断出对应的变量类型的,所以当我们在声明变量便可以给变量赋值时,为了方便起见,我们可以不声明某些变量对应的数据类型。
数据类型
// number
let num: number = 100;
// boolean
let flag: boolean = true;
// string
let message: string = "hello typescript"
//Array
const names: string[] = ["abc", "cba", "nba"]
const names2: Array<string> = ["abc", "cba", "nba"]
// object
const info = {
name: 'zs',
age: 18
}
// symbol
const s1: symbol = Symbol('title')
const s2: symbol = Symbol('title')
const person = {
[s1]: '程序员',
[s2]: '老师'
}
// null和undefined
let n: null = null
let u: undefined = undefined
any类型:在某些情况下,我们确实无法确定一个变量的类型,并且该变量的类型可能会发生一些变化,这个时候我们就可以使用any类型。
unknown类型:用于描述类型不确定的变量。如需要根据条件接收不同的函数调用的返回值,且不同的函数返回值的类型不同时,就可以使用该类型。
any类型和unknown类型的区别:可以将any类型的数据赋值给其他类型,而unknown类型只能赋值给unknown类型或any类型。
void类型:void通常用于指定一个函数是没有返回值的,但可以返回null或undefined。在不指定函数的返回类型时,默认返回值类型就是void
function sum(num1: number, num2: number): void {
// ...
}
never类型:如果一个函数中是一个死循环或者抛出一个异常,那么我们就可以使用never类型。
function loopFn(): never {
while (true) {}
}
never的另一个应用场景:当我们在书写函数时,指定了参数的类型并写了相应类型的逻辑时,如果其他开发人员想调用该函数,并传入一个新类型的参数时,由于之前我们只写了声明函数时对应类型的参数的逻辑,所以此时我们就会缺少相应的逻辑。为了能让我们的函数更健全,就可以使用never,具体示例如下:
function handleMessage(message: number | string) {
switch(typeof message) {
case: 'stirng':
//...
break
case: 'number':
// ...
break
default:
const check: never = message
}
}
当给handleMessage函数的参数添加新类型时,就会执行default下的代码,而给一个never类型的变量赋值时,就会导致编译错误,从而引导开发人员编写相应逻辑的代码。
tuple类型:在数组中通常建议存放相同类型的元素,不建议一个数组中含有不同类型的元素。这个时候我们就可以使用对象或元组,元组中每个元素都有自己特定的类型,且根据索引值获取到的值可以确定对应的类型。
const infoArr: [string, number, number] = ['why', 9, 8]
在定义元组时,需要给其指定初值
应用场景:作为函数返回值
函数的参数类型和返回值类型:通常情况下我们不需要设置函数返回类型的注解,因为ts会根据return的返回值推断函数的返回类型,但为了便于理解,也可以指定返回类型。
function sum(num1: number, num: number): number{
return num1 + num2
}
匿名函数的参数:当一个函数出现在ts可以确定该函数会被如何调用的地方时,该函数的参数类型就会被自动指定。如在forEach的回调函数中,ts会根据forEach的函数类型以及数组的类型推断出item的类型,这个过程称为上下文类型,因为函数执行的上下文可以帮助我们确定参数和返回值的类型。
const names = ['abc', 'cba', 'nba']
names.forEach(item => {
// ...
})
对象类型的参数:我们可以在对象中添加属性,并告知ts该属性需要的是什么类型。如需要指定某个属性是可选的,则可以在属性后面添加?(可以传undefined)
可选类型类似于所指定的类型与undefined的联合类型
function pointInfo(point: {x: number, y?: number}) {
// ...
}
联合类型:联合类型是由两个或多个其他类型组成的类型,联合类型中的每一个类型被称为联合成员
function printId(id: number | string) {
// ...
}
因为参数类型有多种情况,所以我们在函数体内使用时,就需要使用缩小联合。即使用if或者switch来进行判断,缩小我们的代码结构,让ts推断出更具体的类型。
类型别名
功能类似于c++中的typedef,即给一个较长的表达式起一个别名
type Point = {x: number, y: number}
function pointInfo(point: Point) {}
type ID = number | string
function printId (id: ID) {}
类型断言
当我们通过getElementById获取元素时,ts会推断该函数返回HTMLElement类型,而如果我们获取到的是img元素,此时我们是无法对HTMLElement类型添加src属性的,因此我们可以使用类型断言as将其转换为HTMLImageElement
const imgEle = document.getElementById('my-img') as HTMLImageElement
对于类,我们也可以将父类断言为子类。具体看如下案例:
class Person {}
class Student extends Person {
sayHello() {}
}
function foo(p: Person) {
// 可以传Person的子类,但因为参数声明为Person类,所以不可以在函数体内直接调用p.sayHello(),此时,我们就可以使用断言将其转换为Student类
(p as Student).sayHello()
}
const stu = new Student()
foo(stu)
此外,还可以使用as进行强制类型转换,但是不推荐使用
const message: string = "hello as"
const num: number = (message as unknown/any) as number
断言as的应用场景还是比较多的,如想要动态获取state中的数据时,是无法通过
state['${pre}List']来获取的,但我们可以通过断言类型先将其转为any,再动态取值,如(state as any)['${pre}List']
非空类型断言
当函数的参数可以为可选类型时,若在函数体内使用了一些当参数为undefined时会报错的代码时,就会编译不通过。此时我们可以使用类型缩小,也可以使用非空类型的断言,表示我们一定会传入参数,即在使用参数时,在参数后面添加!
function foo (message?: string) {
console.log(message!.length)
}
foo('Hello !')
可选链的使用(js)
如果我们需要获取某个对象中不确定存不存在的元素的属性或方法时,就可以使用可选链。当元素不存在时,直接返回undefined。
type Person = {
name: string,
friend?: {
name: string,
age?: 18
}
}
const info: Person {
name: 'hyk',
friend: {
name: 'kobe'
}
}
console.log(info.friend?.name)
?? 和 !! 的作用(js)
!!:将一个其他类型的变量转换为Boolean类型,相当于两次取反
??:空值合并操作符,当操作符左侧是null或undefined时,返回其右侧操作数,否则返回本身
字面量类型
字面量类型可以认为是我们自定义的类型,当我们将一个变量声明为字面量类型时,那么这个变量的值只能为这个字面量类型的值。一般将其与联合类型一起使用,表示该变量只能去有限个值:
type Alignmen = 'left' | 'right' | 'center'
let message: Alignment = 'left' // (可取值只能是left、right、center)
字面量推理
我们不能将由string类型的实参传递给字面量类型的形参,即使实参的值与字面量的某个类型相同。举例如下:
const info = {
url: 'www.baidu.com',
method: 'GET'
}
function request(url: stirng, method: 'GET' | 'POST') {}
request(info.url, info.method) // 编译失败,string不能转换为字面量类型
解决方案:使用类型断言
// 方法一:
request(info.url, info.method as 'GET')
// 方法二:
const info = {
url: 'www.baidu.com',
method: 'GET'
} as const
类型缩小
我们可以通过类型缩小(type narrowing)来改变ts的执行路径,让ts更明确在某段代码中,某个值的具体类型。
typeof(类型保护)
function printId(id: string | number) {
if (typeof id === 'string') {/*ts推断出此处id为string类型*/}
else {/*ts推断出此处id为number类型*/}
}
平等缩小:使用相等运算符或switch来进行缩小(如===、!==、==、!=)
type Direction = 'left' | 'right' | 'center'
function turnDirection(direction: Direction) {
if (direction === 'left') {/*ts检测出值必为left*/}
else if (direction === 'right') {}
else {}
}
instanceof:用来检查一个对象类型是否是另一个对象类型的实例。
in:用于判断对象是否具有某个属性
函数类型
type CalcFunc = (num1: number, num2: number) => number
// CalcFunc表示类型为函数类型,该函数接收两个number类型的参数,函数的返回值也为number类型
function calc(n1: number, n2: number, fn: CalcFunc) {
return fn(n1, n2);
}
剩余参数
function sum(num1: number, ...num2: number[]) {}
可推导的this类型与不确定的this类型
有时候this指向是可以推导出来的,如在对象的方法中直接使用this时,便可以编译通过,而在函数中直接使用this时,就无法编译通过:
// 可以通过编译及正常运行
const info = {
name: 'hyk',
sayName() {
console.log(this.name)
}
}
info.sayName()
// -------------------------------------
// 无法通过编译
function sayName() {
console.log(this.name)
}
const info = {
name: 'hyk',
sayName
}
info.sayName()
// 解决:在定义函数时,给this添加类型注解
function sayName(this: {name: string}) {
console.log(this.name)
}
函数重载
概念:具有相同函数名称但参数个数或类型不同的多个函数,就称为函数重载。
why?:举个简单的案例,当我们期望对传进来的number或string进行相加操作时,我们是不能够直接进行相加的。因为ts会把这两个参数当成联合类型,两个联合类型的变量不能直接进行相加操作:
function add(n1: number | string, n2: number | string){
return n1 + n2; //'+' cannot be applied to types...
}
当然我们可以通过类型缩小进行实现,但是这也会导致函数的返回值类型不确定的问题。因此,一个新的解决方案出来了:函数重载
function add(n1: number, a2: number): number;
function add(n1: string, n2: string): string;
function add(n1: any, n2: any): any {
return a1 + a2
}
add(20, 30);
add('abc', 'cba');
函数重载的语法:先声明多个不同的重载函数,确定参数个数及类型、返回值类型,但不实现。最后再定义一个具体的实现函数,并且要放宽实现函数的数据类型
类
在ts中,默认情况下是必须给类里面的变量赋值的,否则编译不通过,但是我们可以使用name!:string的方式让其可以不赋初值。
class Person {
name!: string
age: number
constructor (name: string, age: number) {
this.name = name // 可以不给name赋值
this.age = age
}
eating() {
console.log(this.name + 'eating')
}
}
类的继承
在子类的constructor中,若使用到this,则在使用this前,必须先调用super()。也可以在子类的方法中通过super.xxx()来调用父类中的方法。
了解类的多态:多态是允许你将父对象设置成和一个或更多的子对象相等的技术(即将子类类型的指针赋值给父类类型的指针),这样父对象就可以实现调用相同的方法,但产生不同的行为的效果。
类的成员修饰符
在ts中,类的属性和方法支持三种修饰符:public、private、protected
- public修饰的是在任何地方可见、公有的属性或方法,默认情况下就是public
- private修饰的是仅在同一个类中可见、私有的属性或方法
- protected修饰的是仅在类自身及子类中可见、受保护的属性和方法
只读属性readonly
当我们希望类中的属性只可读时,我们就可以在声明变量时使用readonly属性。但要注意的是,该属性是浅只读的,类似于const与vue3中的shallowReadonly
class Person {
readonly name: string
constructor(name: string) {
this.name = name
}
}
getters/setters
当我们设置类中的属性为private时,那么外界就无法访问。此时我们可以设置setXxx/getXxx函数,但还有一个做法,就是设置setter/getter,这样还能起到监听的功能。
class Person {
private name: string
constructor(name: stirng) {
this.name = name
}
set name(newValue) {
this.name = newValue
}
get name() {
return this.name
}
}
抽象类abstract
在定义许多通用的调用接口时,我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,我们可以定义为抽象方法。(抽象方法必须定义在抽象类中,且抽象类不能实例化、抽象方法必须被子类实现)
类的类型
类本身也可以作为一种数据类型,通常用于给对象添加类型注解
class Person {
name: string
constructor(name: string) {
this.name = name
}
running() {
console.log(this.name + " running")
}
}
const p1: Person = {
name: 'stephen',
running() {
// some code
}
}
接口的声明
除了可以通过使用类型别名type来声明一个对象类型外,还可以使用接口来声明
type Point = {
x: number
y: number
}
interface Point {
x: number
y: number
}
在使用接口声明对象类型时,也可以使用只读属性readonly、可选类型?等
索引类型:如果要定义的对象长得比较有规律,就可以使用索引类型:
interface FrontLanguage{
[index: number]: string
}
const frontend: FrontLanguage = {
1: 'html',
2: 'css',
3: 'js'
}
定义函数类型(多数情况下,推荐使用类型别名来定义函数)
interface Func{
(num1: number, num2: number): number
}
接口的实现:如果接口被一个类实现后,那么之后在需要传入接口的地方,都可以将这个类传入
交叉类型
交叉类型与前面个的联合类型形式上类似,但表达的含义有所不同,类似于与的操作,表达的含义时同时满足多个类型。
type MyType = number & string
因为不存在一个变量即属于number类型,又属于string类型,所以MyType其实是一个never类型。
在开发中,交叉类型通常与对象类型进行使用,表示一个对象具有多个不同的属性及类型:
interface Colorful {
color: stirng
}
interface Run {
running: () => void
}
type NewType = Colorful & Run
const obj: NewType = {
color: 'red',
running() {}
}
interface和type的区别
对于非对象类型,推荐使用type,而对于对象类型,推荐使用interface(不过二者都类似)。两者的主要区别是:
- interface可以重复对某个接口定义属性和方法,相同的接口中的属性和方法会进行合并
- 而type定义的是别名,是不能重复的
在定义相同的接口时,若属性名与之前的相同,则类型注解也必须相同
字面量赋值
当我们定义一个接口类型,且假设这个接口类型只定义了3个属性时,若我们想直接给使用该接口类型的对象声明4个属性时,就会报错。而如果我们是先定义一个拥有4个属性的对象,再把这个对象赋值给使用只有3个属性的接口类型的对象时,也可以通过编译。原因是ts会对该种方式进行freshness擦除操作,即把多余的属性先清除,若剩下的属性符合要求,则可以编译通过。也可以使用这种方式给函数传参。
interface Person {
name: string,
eatring: () => void
}
// -----------
const p: Person = {
name: "why",
age: 18, // 报错
eating() {}
}
// -----------
const pInfo = {
name: "why",
age: 18,
eating() {}
}
const person: Perosn = info // 编译通过
枚举类型
枚举类型是ts特有的特性之一。枚举就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型。
enum Direction {
LEFT,
RIGHT,
TOP,
BOTTOM
}
// 具体使用
function turnDirection(direction: Direaction) {
switch (direction) {
case: Direction.LEFT:
// ...
break;
case: Direction.RIGHT:
// ...
break;
// ...
}
}
枚举类型是有值的,默认情况下,为0, 1, 2, 3, 4 ...,我们可以直接给其赋各种类型的值
泛型
泛型核心:函数的参数类型参数化
使用泛型,主要是希望我们的代码具有更强的可重用性。功能之一是由使用者决定传入函数的参数具体是什么类型,而不是由编写函数的开发者在一开始就确定了使用函数时应该传递的参数类型。
基本使用:
function foo<T>(arg: T): T {
return arg
}
// 调用方式一:
foo<number>(789)
// 调用方式二:这里会推导出我们传入的参数是字面量类型
foo("abc")
传入多个泛型:
function foo<T, E>(x: T, y: E) {}
泛型约束:有时我们希望传入的类型具有某些共性,但这些共性可能不在同一种类型,此时我们就可以使用泛型约束:
interface Length {
length: number
}
function getLength<T extends Length>(args: T) {
return args.length
}
getLength("abc")
getLength(['abc', 'asdjk'])
getLength({length: 8, name: 'hyk'})
注意:这里推导出来的不是字面量类型,而是能准确推出具体的类型
命名空间namespace
我们知道有模块作用域,但如果我们还想细分作用域,就可以使用namespace:
export namespace time {
export function format(time: stirng) {}
export let num: number = 12
}
export namespace price {
export function format(price: number) {}
}
// 通过time.format调用相关函数,未暴露的变量或函数无法获取
类型的查找
在之前我们使用ts的过程中,所用到的类型,大多都是我们自己编写的。而有一些类型如HTMLImageElement等,我们并未编写,那么它来自哪里呢?其实ts有自己的管理和查找规则,它会查找以.d.ts为后缀名的文件,.d.ts文件是用来做类型的声明的,仅仅用于类型检测,告诉ts一共有哪些类型。一共有三种类型声明,分别是:
-
内置类型声明,这是ts自带的,用于帮助我们内置js运行时的一些标准化API的声明文件,如Math、Date、Document等。
-
外部定义类型声明,即我们使用一些第三方库时,需要的一些类型声明。这些类型声明可能会存在于自己的库中,如axios等。也可能不存在自己的库中,需要我们到特定的地方去找,下面给出两个查找一些第三方库的类型声明文件的url:
-
自定义声明文件,即在上述两种情况中都不存在类型声明的文件。
// 声明模块
declare module 'lodash' {
export function join(arr: any[]): void
}
// 声明变量/函数/类
declare let whyName: string
declare let whyAge: number
declare let whyHeight: number
declare function whyFoo(): void
declare class Person { // 声明构造函数类型,也用class
name: string
age: number
constructor(name: string, age: number)
}
// 声明文件
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.png'
declare module '*.svg'
declare module '*.gif'
// 声明命名空间
declare namespace $ {
export function ajax(settings: any): any
}
InstanceType
我们可以认为一个vue文件中导出的是一个对象描述符,当在template中使用时,vue就会帮我们根据对象描述符创建一个组件实例。而在setup中我们是不可以直接使用对象描述符的,此时我们可以借助InstanceType,该函数接收一个对象描述符的类型,并返回该对象描述符所对应的构造函数类型的实例类型。
ref结合ts
当我们定义一个ref对象,并想修改ref对象中的某个值时,就需要指定ref对象的具体类型,否则无法通过编译,ts会认为某个值不一定存在。
const myRef = ref<any>({})
myRef.value.name = 'zs'