02学习TypeScript
1. 联合类型和交叉类型
1.1联合类型(Union Type)
定义:从现有类型中构建新类型,由两个或者多个其他类型组成的类型;表示可以是这些类型中的任何一个值;联合类型中的每一个类型被称之为联合成员(union’s members) ;
// 打印id
function printID(id:number | string){
console.log("您的ID:",id);
//类型缩小,确定id是string类型
if(typeof id === "string"){
console.log(id.length,id.toUpperCase());
}else{
//确定id是number类型
console.log(id);
}
}
printID("abc")
printID(123)
1.2 类型别名
在介绍交叉类型之前,需要介绍类型别名
在类型注解中编写对象类型和联合类型,但是当我们想要多次在其他地方使用时,就要编写多次。
// 类型别名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);
}
printCoordinate({x:10,y:20})
1.3 接口声明
通过type可以用来声明一个对象类型:
type PointType = {
x: number,
y: number,
z?: number
}
另外一种声明对象类型方式就是通过接口来声明:
// 接口:interface
// 声明的方式
interface PointType2 {
x: number,
y: number,
z?: number
}
类型别名和接口非常相似,在定义对象类型时,大部分时候,你可以任意选择使用。接口的几乎所有特性都可以在type 中使用(后续我们还会学习interface的很多特性);
接口声明和别名区别:
// 1.区别一: type类型使用范围更广 接口类型只能用来声明对象
type myNumber = number
type IDType = number | string
// 2.区别二:在声明对象时,interface可以多次声明
// 2.1. type不允许两个相同名称的别名同时存在
// 2.2. interface可以多次声明同一个接口名称
interface PointType2 {
x: number,
y: number
}
interface PointType2 {
z?: number
}
const point: PointType2 = {
x: 100,
y: 90
}
// 3.interface支持继承的
interface IAnimal {
category: string
}
interface ICat extends IAnimal {
name: string,
age: number
}
const cat1: ICat = {
category: "猫",
name: "花花",
age: 2
}
// 4.interface可以被类实现(TS面向对象时候再讲)
export { }
总结: 如果是非对象类型的定义使用type,如果是对象类型的声明那么使用interface,扩展更好
1.4 交叉类型
- 交叉类型表示需要满足多个类型的条件;
- 交叉类型使用&符号;
// 回顾: 联合类型
type ID = number | string
const id1: ID = "abc"
const id2: ID = 123
// 交叉类型: 两种(多种)类型要同时满足
type NewType = number & string // 没有同时满足是一个number又是一个string的值,所以NewType是一个never类型;
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")
}
}
2. 类型断言和非空断言
2.1 类型断言as
有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)
比如我们通过document.querySelector(“.img”),TypeScript只知道该函数会返回HTMLElement,但并不知道它具体的类型:
TypeScript只允许类型断言转换为更具体或者不太具体的类型版本
// 使用类型断言,转换为更具体
const imgEl = document.querySelector(".img") as HTMLImageElement
imgEl.src = "xxx"
imgEl.alt = "yyy"
2.2 非空类型断言
非空断言使用的是!,表示可以确定某个标识符是有值的,跳过ts在编译阶段对它的检测;
interface IPerson{
name:string,
age:number,
friend?:{
name:string
}
}
const info:IPerson = {
name:"haha",
age:10
}
//访问属性: 可选链: ?.
console.log(info.friend?.name);
// 属性赋值不能用可选链
// 解决方案一:类型缩小
if(info.friend){
info.friend.name = "kobe"
}
// 解决方案二:非空类型断言!(有点危险,, 只有确保friend一定有值的情况, 才能使用)
info.friend!.name = "james"
export {}
3. 字面量类型和类型缩小
3.1 字面量类型
将多个字面量类型联合起来
// 将多个字面量类型联合起来 |
type Direction = "left" | "right" | "up" | "down"
const d1: Direction = "left"
对象在进行字面量推理的时候,info其实是一个{url: string, method: string},所以没办法将一个string赋值给一个字面量类型
// 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)
3.2 类型缩小(Type Narrowing)
- 我们可以通过类似于
typeof padding === "number”
的判断语句,来改变TypeScript的执行路径; - 在给定的执行路径中,可以缩小比声明时更小的类型,这个过程称之为缩小(Narrowing ) ;
typeof padding === "number
可以称之为类型保护(type guards) ;
常见的类型保护种类:
- 1.typeof
// 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)
- 等等…
4. 函数的类型和函数签名
4.1 函数类型
// 方案一: 函数类型表达式function type expression
// 格式: (参数列表) => 返回值类型
type BarType = (num1: number) => number //函数类型
const bar:BarType = (arg:number):number=>{
return 123
}
函数类型练习:由函数外部决定+ - * /运算
//定义函数类型
type callBackType = (num1: number, num2: number) => number
// 1.函数的定义
function calc(callBackFn: callBackType) {
const num1 = 10
const num2 = 20
const res = callBackFn(num1, num2)
return res
}
// 2.函数的调用
// 2.1 匿名函数参数不需要写类型,会自动上下文推导
console.log(
calc(function (num1, num2) {
return num1 + num2
})
);
// 2.2
function add(num1:number,num2:number){
return num1 + num2
}
console.log("相加:",calc(add));
export { }
TypeScript对于传入的函数类型的多余的参数会被忽略掉(the extra arguments are simply ignored)
原因:大多函数都是匿名函数,比如forEach函数的三个参数大多数情况都用不到,一般用第一个参数item就够了,必须要写全部的三个参数反而影响TS使用体验
只传入一个参数也可以:function foo(num1:number){return num1} calc(foo);
错误的:function foo(num1:string){return num1} calc(foo);
4.2 函数签名
调用签名
函数也是个对象,可以有自己的属性值
前面讲到的函数类型表达式并不能支持声明属性;
如果想描述一个带有属性的函数,可以在一个对象类型中写一个调用签名(call signature) ;
开发中如何选择:
- 如果只是描述函数类型本身(函数可以被调用), 使用函数类型表达式(Function Type Expressions)
- 如果在描述函数作为对象可以被调用, 同时也有其他属性时, 使用函数调用签名(Call Signatures)
// 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(123)
bar.name = "aaa"
bar.age = 18
构造签名
class Person {
}
interface ICTORPerson {
new (): Person
}
function factory(fn: ICTORPerson) {
const f = new fn()
return f
}
//传入的是可以new的构造函数和类
factory(Person)
5. 函数的参数
5.1可选参数
指定某个参数可选,可选类型需要在必传参数的后面
// y就是一个可选参数
// 可选参数类型是什么? number | undefined 联合类型
function foo(x: number, y?: number) {
//y还有可能是undefined,需要类型缩小(收缩)
if (y !== undefined) {
console.log(y + 10)
}
}
foo(10)
foo(10, 20)
export {}
5.2参数的默认值
// 函数的参数可以有默认值
// 1.有默认值的情况下, 参数的类型注解可以省略
// 2.有默认值的参数, 是可以接收一个undefined的值
function foo(x: number, y = 100) {
console.log(y + 10)
}
foo(10)
foo(10, undefined)
foo(10, 55)
export { }
5.3剩余参数
function foo(...args:(string|number)[]){
}
foo(123,321)
foo('a','b','c',1)
6. 函数的重载和this类型
6.1函数的重载(了解)
实现只能将两个数字/两个字符串进行相加:
- 在TypeScript中,我们可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用;
- 一般是编写两个或者以上的重载签名,再去编写一个通用的函数以及实现;
// 需求: 只能将两个数字/两个字符串进行相加
// TypeScript中函数的重载写法
// 1.先编写重载签名
function add(arg1: number, arg2: number): number
function add(arg1: string, arg2: string): string
// 2.编写通用的函数实现
function add(arg1: any, arg2: any): any {
return arg1 + arg2
}
add(10, 20)
add("aaa", "bbb")
// 通用函数不能被调用,没有与此调用匹配的重载。
// add({name: "why"}, "aaa")
// add("aaa", 111)
export {}
6.2联合类型和函数重载选择
定义一个函数,可以传入字符串或者数组,获取它们的长度。
可以使用联合类型实现的情况, 尽量使用联合类型
实现方案:
- 使用联合类型
- 使用函数重载
- 使用对象类型(包含Length属性)
//1.联合类型实现(可以使用联合类型实现的情况, 尽量使用联合类型)
/*
function getLength(arg:string|any[]) {
return arg.length
}
*/
// 2.函数的重载
/*
function getLength(arg:string):number
function getLength(arg:any[]):number
function getLength(arg){
return arg.length
}
*/
// 3.对象类型实现
function getLength(arg:{length:number}){
return arg.length
}
getLength("abc")
getLength(["a",1,3])
getLength({ length: 100 })
export { }
6.3可推导的this类型
王红元(coderwhy)老师的javascript讲解this的文章地址.
函数中this的默认类型
// 在没有对TS进行特殊配置的情况下, this是any类型
// 1.对象中的函数中的this
const obj = {
name: "why",
studying: function () {
// 默认情况下, this是any类型
console.log(this.name.length, "studying")
}
}
obj.studying()
//危险的,指定this为{} undefined.undefined
// obj.studying.call({})
// 2.普通的函数
function foo() {
console.log(this)
}
export { }
初始化生成配置文件tsconfig.json:tsc --init
在设置配置选项(编译选项compilerOptions, noImplicitThis设置为true, 不允许模糊的this存在)
//tsconfig.json:不能有模糊的this
"noImplicitThis": true
指定this的类型
函数的第一个参数可以根据该函数之后被调用的情况,用于声明this的类型(名词必须叫this) ;
在后续调用函数传入参数时,从第二个参数开始传递的,this参数会在编译后被抹除;
function foo(this: { name: string }, info: { name: string }) {
console.log(this, info)
}
foo.call({ name: "why" }, { name: "kobe" })
this相关的内置工具
- ThisParameterType:
function foo(this: { name: string }, info: { name: string }) {
console.log(this, info)
}
type FooType = typeof foo
// 1.ThisParameterType: 获取FooType类型中this的类型
type FooThisType = ThisParameterType<FooType>
export { }
- OmitThisParameter:
function foo(this: { name: string }, info: { name: string }) {
console.log(this, info)
}
type FooType = typeof foo
// 2.OmitOmitThisParameter: 删除this参数类型, 剩余的函数类型
type PureFooType = OmitThisParameter<FooType>
export { }
- ThisType :
function foo(this: { name: string }, info: { name: string }) {
console.log(this, info)
}
type FooType = typeof foo
// 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 { }
写在最后
本篇文章到这里就结束了,如果文章对你有用,可以三连支持一下,如果文章中有错误或者说你有更好的见解,欢迎指正~
代码地址: lemonDin/learn_typescript(github.com).
PS:代码和思路来自王红元(coderwhy)老师,在其中做个总结