TS相关知识了解
概述
本篇我们来讲述一下TS中的常见但又有些难度的小知识。
联合类型 |
TypeScript 的类型系统允许我们使用多种运算符,从现有类型中构建新类型
我们来使用第一种组合类型的方法: 联合类型(Union Type)
- 联合类型是由两个或者多个其他类型组成的类型
- 表示 可以是这些类型中的
任何
一个值 - 联合类型中的每一个被称之为联合成员(union’s members )
// 1.联合类型的基本使用
// let foo: number | string = "abc"
// foo = 123
// // 使用的时候要特别的小心
// if (typeof foo === "string") {
// console.log(foo.length)
// }
// 2.举个栗子: 打印id
function printID(id: number | string) {
console.log("您的ID:", id)
// 类型缩小
if (typeof id === "string") {
console.log(id.length)
} else {
console.log(id)
}
}
printID("abc")
printID(123)
export {}
类型别名 type
在前面,我们通过类型注解中编写对象类型和联合类型,但是当我们想要多次在其他地方使用时就编写多次。这时我们可以给对象类型起一个别名:
// 类型别名: type
type MyNumber = number
const age: MyNumber = 18
// 给ID的类型起一个别名
type IDType = number | string
function printID(id: IDType) {
console.log(id)
}
// 打印坐标
type PointType = { x: number, y: number, z?: number }
function printCoordinate(point: PointType) {
console.log(point.x, point.y, point.z)
}
export {}
接口的声明 interface
const name = "why"
console.log("why")
console.log(name)
type PointType = {
x: number
y: number
z?: number
}
// 接口: interface
// 声明的方式
interface PointType2 {
x: number
y: number
z?: number
}
function printCoordinate(point: PointType2) {
}
export {}
type与interface的区别
我们会发现interface和type都可以用来定义对象类型,那么在开发中时到底选择哪一个呢?
- 如果是定义非对象类型,通常推荐使用type。
- 如果是定义对象类型,那么他们有区别的:
- interface 可以重复的对某个接口来定义属性和方法;
- 而type 定义的是别名,不能重复。
// 1.区别一: type类型使用范围更广, 接口类型只能用来声明对象
type MyNumber = number
type IDType = number | string
// 2.区别二: 在声明对象时, interface可以多次声明
// 2.1. type不允许两个相同名称的别名同时存在
// type PointType1 = {
// x: number
// y: number
// }
// type PointType1 = {
// z?: number
// }
// 2.2. interface可以多次声明同一个接口名称
interface PointType2 {
x: number
y: number
}
interface PointType2 {
z: number
}
const point: PointType2 = {
x: 100,
y: 200,
z: 300
}
// 3.interface支持继承的
interface IPerson {
name: string
age: number
}
interface IKun extends IPerson {
slogan: string
}
const ikun1: IKun = {
slogan: "你干嘛, 哎呦",
name: "kobe",
age: 30
}
// 4.interface可以被类实现(TS面向对象时候再讲)
// class Person implements IPerson {
// }
// 总结: 如果是非对象类型的定义使用type
// 如果是对象类型的声明那么使用interface
export {}
所以, interface可以为现有的接口提供更多扩展。
交叉类型 &
- 交叉类似表示需要满足多个型的条件
- 交叉类型使用
&
符号
// 回顾: 联合类型
type ID = number | string
const id1: ID = "abc"
const id2: ID = 123
// 交叉类型: 两种(多种)类型要同时满足
type NewType = number & string // 没有意义
interface IKun {
name: string
age: number
}
interface ICoder {
name: string
coding: () => void
}
type InfoType = IKun & ICoder
const info: InfoType = {
name: "why",
age: 18,
coding: function() {
console.log("coding")
}
}
export {}
类型断言 as
有时候TypeScript 无法获取具体的类型信息,这个时候我们需要使用类型断言(Type Assertions)
类型断言的规则: 断言只能断言成更加具体的类型, 或者不太具体(any/unknown) 类型。此规则可防止不可能的强制转换
// 获取DOM元素 <img class="img"/>
// const imgEl = document.querySelector(".img")
// if (imgEl !== null) { // 类型缩小
// imgEl.src = "xxx"
// imgEl.alt = "yyy"
// }
// 使用类型断言
const imgEl = document.querySelector(".img") as HTMLImageElement
imgEl.src = "xxx"
imgEl.alt = "yyy"
// 类型断言的规则: 断言只能断言成更加具体的类型
// 或者不太具体(any/unknown) 类型
const age: number = 18
// 错误的做法
// const age2 = age as string
// TS类型检测来说是正确的, 但是这个代码本身不太正确
// const age3 = age as any
// const age4 = age3 as string
// console.log(age4.split(" "))
export {}
非空类型断言言 !
非空断言使用的是 !
,表示可以确定某个标识符是有值的,跳过 ts
在编译阶段对它的检测
// 定义接口
interface IPerson {
name: string
age: number
friend?: {
name: string
}
}
const info: IPerson = {
name: "why",
age: 18
}
// 访问属性: 可选链: ?.
console.log(info.friend?.name)
// 属性赋值:
// 解决方案一: 类型缩小
if (info.friend) {
info.friend.name = "kobe"
}
// 解决方案二: 非空类型断言
// (有点危险, 只有确保friend一定有值的情况, 才能使用)
info.friend!.name = "james"
export {}
字面量类型
// 1.字面量类型的基本使用:没有什么意义
const name: "why" = "why"
let age: 18 = 18
// 2.将多个字面量类型联合起来 |
type Direction = "left" | "right" | "up" | "down"
const d1: Direction = "left"
// 栗子: 封装请求方法
type MethodType = "get" | "post"
function request(url: string, method: MethodType) {
}
request("http://xxx.xx.xx", "post")
// TS细节
// const info = {
// url: "xxxx",
// method: "post"
// }
// 下面的做法是错误: info.method获取的是string类型
// request(info.url, info.method)
// 解决方案一: info.method进行类型断言
// request(info.url, info.method as "post")
// 解决方案二: 直接让info对象类型是一个字面量类型
// const info2: { url: string, method: "post" } = {
// url: "xxxx",
// method: "post"
// }
const info2 = {
url: "xxxx",
method: "post"
} as const
// xxx 本身就是一个string
request(info2.url, info2.method)
export {}
类型缩小
什么是类型缩小呢?
- 类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄);
- 我们可以通过类似于 typeof padding === “number” 的判断语句,来改变 TypeScriptTypeScript 的执行路径 ;
- 在给定的执行路径中,我们可以缩小比声明时更小的类型 ,这个过程称之为缩小(Narrowing )
- 而我们编写的 typeof padding === “number” 可以称之为 类型保护(type guards )。
常见的类型保护有如下几种: typeof
- 平等缩小(
=
、!
) instanceof
in
等
// 1.typeof: 使用的最多
function printID(id: number | string) {
if (typeof id === "string") {
console.log(id.length, id.split(" "))
} else {
console.log(id)
}
}
// 2.===/!==: 方向的类型判断
type Direction = "left" | "right" | "up" | "down"
function switchDirection(direction: Direction) {
if (direction === "left") {
console.log("左:", "角色向左移动")
} else if (direction === "right") {
console.log("右:", "角色向右移动")
} else if (direction === "up") {
console.log("上:", "角色向上移动")
} else if (direction === "down") {
console.log("下:", "角色向下移动")
}
}
// 3. instanceof: 传入一个日期, 打印日期
function printDate(date: string | Date) {
if (date instanceof Date) {
console.log(date.getTime())
} else {
console.log(date)
}
// if (typeof date === "string") {
// console.log(date)
// } else {
// console.log(date.getTime())
// }
}
// 4.in: 判断是否有某一个属性
interface ISwim {
swim: () => void
}
interface IRun {
run: () => void
}
function move(animal: ISwim | IRun) {
if ("swim" in animal) {
animal.swim()
} else if ("run" in animal) {
animal.run()
}
}
const fish: ISwim = {
swim: function() {}
}
const dog: IRun = {
run: function() {}
}
move(fish)
move(dog)
export {}
函数类型
函数类型的表示方法
function foo(arg: number): number {
return 123
}
export {}
如上例所示,函数foo的参数和返回值都有自己的类型,但是foo本身也是一个标识符, 也应该有自己的类型。
// 格式: (参数列表) => 返回值
type BarType = (num1: number) => number
const bar: BarType = (arg: number): number => {
return 123
}
export {}
函数类型参数的格式
有两条tips:
- TypeScript对于传入的函数类型的多余的参数会被忽略掉
- TS对于很多类型的检测报不报错, 取决于它的内部规则,TS版本在不断更新: 在进行合理的类型检测的情况,让ts同时更好用(好用和类型检测之间找到一个平衡)
// TypeScript对于传入的函数类型的多余的参数会被忽略掉
// (the extra arguments are simply ignored.)
type CalcType = (num1: number, num2: number) => number
function calc(calcFn: CalcType) {
calcFn(10, 20)
}
calc(function(num) {
return 123
})
// forEach栗子:
const names = ["abc", "cba", "nba"]
names.forEach(function(item) {
console.log(item.length)
})
// TS对于很多类型的检测报不报错, 取决于它的内部规则
// TS版本在不断更新: 在进行合理的类型检测的情况
// 让ts同时更好用(好用和类型检测之间找到一个平衡)
// 举一个栗子:
interface IPerson {
name: string
age: number
}
// typescript github issue, 成员
const p = {
name: "why",
age: 18,
height: 1.88,
address: "广州市"
}
// 这里之所以不报错,按照TS团队成员在GitHub上的回答就是:
// 因为此时p不是初次赋值,不fresh了
const info: IPerson = p
export {}
调用签名
在TypeScript 中,函数除了可以被调用,自己也是可以有属性值的。
- 然而前面讲到的函数类型表达式,并不能支持声明属性
- 如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名( call signature)
// 1.函数类型表达式
type BarType = (num1: number) => number
// 2.函数的调用签名(从对象的角度来看待这个函数, 也可以有其他属性)
interface IBar {
name: string
age: number
// 函数可以调用: 函数调用签名
(num1: number): number
}
const bar: IBar = (num1: number): number => {
return 123
}
bar.name = "aaa"
bar.age = 18
bar(123)
// 开发中如何选择:
// 1.如果只是描述函数类型本身(函数可以被调用)
// 使用函数类型表达式(Function Type Expressions)
// 2.如果在描述函数作为对象可以被调用, 同时也有其他属性时
// 使用函数调用签名(Call Signatures)
export {}
注意这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是:
而不是=>
构造签名
JavaScript函数也可以使用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数 ( constructors), 因为他们会产生一个新对象。
可以写一个 构造签名
(Construct Signatures )方法,是在 调用签名
加一个 new
关键词;
class Person {
name: string,
constructor(name: string) {
this.name = name
}
}
interface IPerson {
new (name: string): Person
}
function factory(P: IPerson) {
return new P('Lee')
}
函数的参数:可选参数
// y就是一个可选参数
// 可选参数类型是什么? number | undefined 联合类型
function foo(x: number, y?: number) {
if (y !== undefined) {
console.log(y + 10)
}
}
foo(10)
foo(10, 20)
export {}
函数的参数:参数默认值
函数的参数可以有默认值
- 有默认值的情况下, 参数的类型注解可以省略
- 有默认值的参数, 是可以接收一个undefined的值
// 这个时候的y是number和undefined的联合类型
function foo(x: number, y = 100) {
console.log(y + 10)
}
foo(10)
foo(10, undefined)
foo(10, 55)
export {}
函数的参数:剩余参数
从ES6 开始,JavaScript也支持剩余参数,语法允许我们将一个不定量的放到组中。
function foo(...args: (string | number)[]) {
}
foo(123, 321)
foo("abc", 111, "cba")
export {}
函数的重载
什么是函数的重载,我们来看一下下面这个案例
// 需求: 只能将两个数字/两个字符串进行相加
// 案例分析: any实现
// function add(arg1, arg2) {
// return arg1 + arg2
// }
// add(10, 20)
// add("abc", "cba")
// add({aaa: "aaa"}, 123)
// 1.实现两个函数
// function add1(num1: number, num2: number) {
// return num1 + num2
// }
// function add2(str1: string, str2: string) {
// return str1 + str2
// }
// add1(10, 20)
// add2("abc", "cba")
// 2.错误的做法: 联合类型不可以,因为不能确定具体类型
// function add(arg1: number|string, arg2: number|string) {
// return arg1 + arg2
// }
// 3.TypeScript中函数的重载写法
// 3.1.先编写重载签名
function add(arg1: number, arg2: number): number
function add(arg1: string, arg2: string): string
// 3.2.编写通用的函数实现
function add(arg1: any, arg2: any): any {
return arg1 + arg2
}
add(10, 20)
add("aaa", "bbb")
// 通用函数不能被调用
// add({name: "Lee"}, "aaa")
// add("aaa", 111)
export {}
从上面的案列我们知道实现函数重载的步骤为:
- 编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用
- 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现。
- 如上例所示,通用函数是不能被调用的。这句话应该这样理解:通用函数是不能按照不符合重载签名的方式进行调用的。也就是上例中只能单独实现数字或者字符串的相加,不能实现其他类型的运算
函数重载和联合类型的对比
我们现在有一个需求:定义一个函数,可以传入字符串或数组,并且获取它的长度。
// 1.普通的实现
// function getLength(arg) {
// return arg.length
// }
// 2.函数的重载
// function getLength(arg: string): number
// function getLength(arg: any[]): number
// function getLength(arg) {
// return arg.length
// }
// 3.联合类型实现(可以使用联合类型实现的情况, 尽量使用联合类型)
// function getLength(arg: string | any[]) {
// return arg.length
// }
// 4.对象类型实现
function getLength(arg: { length: number }) {
return arg.length
}
getLength("aaaaa")
getLength(["abc", "cba", "nba"])
// 报错,传入的参数类型不符合定义
getLength({ name: "why", length: 100 })
export {}
从上例我们得出的结论就是:当使用函数重载和联合类型都能实现的情况下,尽量使用联合类型
函数中的this类型
this默认类型, 在没有对TS进行特殊配置的情况下, this是any类型
// 1.对象中的函数中的this
const obj = {
name: "why",
studying: function() {
// 默认情况下, this是any类型
console.log(this.name.length, "studying")
}
}
obj.studying()
// obj.studying.call({})
// 2.普通的函数
function foo() {
console.log(this)
}
export {}
this明确类型
如何指定this的确切类型呢?使用函数的第一个参数类型:
- 函数的第一个参数我们可以根据该函数之后被调用的情况,用于声明 this的类型(名词必须叫this)
- 在后续调用函数传入参数时, 是从第二个参数开始传递的,this参数会在编译后被抹除。
// 在设置配置选项(编译选项compilerOptions, noImplicitThis设置为true
// 不允许模糊的this存在)
// 1.对象中的函数中的this
const obj = {
name: "why",
studying: function(this: {}) {
// 默认情况下, this是any类型
console.log(this, "studying")
}
}
// obj.studying()
obj.studying.call({})
// 2.普通的函数
function foo(this: { name: string }, info: {name: string}) {
console.log(this, info)
}
foo.call({ name: "James" }, { name: "Kobe" })
export {}
this相关的内置工具
Typescript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用。主要有以下3个:
- ThisParameterType :
- 用于提取一个
函数类型Type
的this (opens new window) 参数类型 - 如果这个函数类型没有
this
参数则返回unknown
- OmitThisParameter:
- 用于移除一个函数类型Type的this参数类型 , 并且返回当前的函数类型
- ThisType:
- 这个类型不返回一个转换过的类型,它被用作标记上下文的this类型
function foo(this: { name: string }, info: {name: string}) {
console.log(this, info)
}
type FooType = typeof foo
// 1.ThisParameterType: 获取FooType类型中this的类型
type FooThisType = ThisParameterType<FooType>
// 2.OmitOmitThisParameter: 删除this参数类型, 剩余的函数类型
type PureFooType = OmitThisParameter<FooType>
// 3.ThisType: 用于绑定一个上下文的this
interface IState {
name: string
age: number
}
interface IStore {
state: IState
eating: () => void
running: () => void
}
const store: IStore & ThisType<IState> = {
state: {
name: "why",
age: 18
},
eating: function() {
console.log(this.name)
},
running: function() {
console.log(this.name)
}
}
store.eating.call(store.state)
export {}
总结
从以上的分析我们可以看出,我们本篇文章所写的大部分知识都与我们掌握的 JS 知识差不多,只是增加了类型检测与约束而已。