typescript 基础一篇掌握(一万四千字攻略总结)

前言

今天来记录一下ts的基础语法,因为除了造轮子必须对工具进行一定的类型限制,还有要想搭建优质系统化的前端工程,ts仍是必不可少的。

本文编写了很久,看了很多文章,因为知识点存在交叉,我用自己比较好理解的方式排版了内容,希望能够帮助到想要学习ts语法的同学们。

如果在观看前面内容时出现不解的地方,可以先忽略,继续往下看完,再回过头来也许就可以融会贯通了。

类型声明

一、基本类型

我们知道的js的基本数据类型:number、string、boolean、underfined、null、symbol、bigInt

// 先声明类型再赋值
let a: number
a = 1

// 声明的同时赋值
let b: number = 1
let c: string = '1'
let d: boolean = true
let e: null = null
let f: undefined = undefined
let g: symbol = Symbol(1)
let h: bigint = 1n

这里有一点很特殊需要注意。

非严格模式下nullunderfined可以赋值给其他类型(这里的其他类型不仅仅包括当前介绍的基本数据类型,还有一些对象类型等,就不举例说明),建议编写ts时都在严格模式下进行

// 非严格模式下编译正确,严格模式下错误
let a: number = null
let b: string = undefined

严格模式下null只能赋值给null,而undefined只能赋值给undefined或者void

二、特殊类型

特殊类型包括(any、unknown、void、never)。

1. any

即接受任何类型,也就是未给变量声明类型且未赋值时,则默认any,详见类型推论

let a: any = 123

2. unknown

any很相似,同样接受任何类型。

any区别是:any可以接受任何类型同时也可以赋值给任何类型,而unknown虽然能接受任何类型但是只能赋值给unknownany

let a: any = 123
let b: string = a // 正确,any类型的值可以赋值给任意类型

let c: unknown = 123
let d: string = c  // 错误,unknown类型的值只可以赋值给unknown和any类型

3. void

可以理解为空类型,多用于函数没有返回值时,给函数返回值类型声明为void,如果非要返回类型的话,只接受undefined(非严格模式下还会接受null),但是这是没有意义的,建议是不要返回值。

void通常用于函数类型的返回值:

let func = (): void => {
    return undefined
}

4. never

表示绝对不会有值的类型。never不能被其他任何类型赋值。用的比较少。

三、对象类型

1.对象

对象类型声明形式。

let obj1: object = {}

对象类型其实还有另外两种声明形式,但是不建议使用,因为他们可以接受所有拥有toString和hasOwnProperty方法的类型,这会导致,基本数据类型也可以给他们赋值,就没办法达到我们单纯想要对象类型的本意了。

let obj2: Object = {}
let obj3: {} = {} 
let obj2: Object = 123 // 仍然正确
let obj3: {} = 123 // 仍然正确

通常我们声明对象时,不会单纯使用object,我们大多数时候需要声明一个更具体的对象出来,对象里的每个键值我们都需要声明好对象。

let pe: {
    name: string
    age: number
}

pe = {
    name: '小明',
    age: 18
}

2.数组

数组有两种声明形式,一种是Array一种是[]

使用时得声明数组中接受的类型,比如字符串:

let a: Array<string>
let b: string[]

如果允许接受任何类型的话,可以设置为:

let a: Array<any>
let b: []
//或 let b: any[]
元组

还有一种特殊的数组声明方式,称为元组类型,必须要知道具体的数组长度和数组中每个元素一一对应的类型,用的也比较少:

let arr: [number, string]
// 长度和对应的对应下标对应的类型都必须一致
arr = [123, '123']

四、函数类型

字面量声明

函数类型的字面量用{ (入参类型): 返回值类型 }形式或(入参类型) => 返回值类型表示:

let func: { (a: number, b: string): string } = (a, b) => {
    return a + b
}

let func: (a: number, b: string) => string  = (a, b) => {
    return a + b
}

在函数体中声明

当然我们有时候不一定需要用字面量,我们可以在函数声明时,对函数出现的入参属性和返回值直接进行赋值。

对入参声明对象很简单,也就是对括号中的每个入参逐一进行类型声明。

let func = (a: number, b: string) => {}

对返回值声明只要在函数那个括号后声明即可:

let func = ():string => { 
    return 'str'
}

合起来就是:

let func = (a: number, b: string): string => {
    return 'str'
}

非箭头函数道理也是一样的:

function func(a: number, b: string): string {
    return 'str'
}

五、值类型

可以给对象声明固定的值,只允许接受该值。

let str: 'string'
str = 'string'

这样有什么用呢?比如某个函数需要的某个入参,是固定的某些值,结合联合类型,我们就可以这样控制别人调用我们函数时的入参必须在这些值的可选范围内了,

再看例子:

let func = (type: 'type1' | 'type2') => {
    switch (type) {
        case "type1":
           ...
        case "type2":
           ...
    }
}

声明类型关键字

我们前面都是在考虑使用某个变量时,直接对它进行类型声明再去赋值

不过这样并不利于类型的复用,面对一些复杂的类型时,我们可以先用关键字声明出一个类型,然后该类型可以用于你进行类型声明,这样做的好处就是,声明出来的类型可以被复用。

一、类型别名type

声明类型时,我们使用的是type关键字,我们在上文类型声明中提到的所有类型都可以用type来声明。

这种方式也被称为类型别名,就是给类型创建了一个新名称。

type Str = string

let str1: Str = '123'
let str2: Str = '123'

这时候有人会问了,你这有啥用啊?我就不能直接声明string吗,非要声明一个类型出来。

别急,对于这种简单的基本类型,确实没意义,而假如是我们要复用某个联合类型,如果我们不声明类型的话就会变得非常复杂:

let str1: string | number | undefined = '123'
let str2: string | number | undefined = 123
let str3: string | number | undefined = undefined

而声明一个类型出来,就清晰多了:

type StrOrNumOrUn = string | number | undefined

let str1: StrOrNumOrUn = '123'
let str2: StrOrNumOrUn = 123
let str3: StrOrNumOrUn = undefined

二、接口interface

1. 声明对象

声明复用对象类型时用的关键字是interface,表示的是接口含义,学过面对对象的人很好理解。

interface PeObj {
    name: string,
    age: number
}

let pe1: PeObj = {
    name: '小明',
    age: 20
}

let pe2: PeObj = {
    name: '小红',
    age: 18
}

声明类型时,可以对不同的类型进行交叉操作,进行另外的复用:交叉类型

2. 声明函数

interface Func {
    (a: string, b: number): string
}

let func1: Func = (a, b) => {
    return a + b
}

let func2: Func = (c, d) => {
    return c
}

注意

上述interface可以用于声明具体的对象和函数,用type同样可以做到

type PeObj = {
    name: string,
    age: number
}

type Func = {
    (a: string, b: number): string
}

三、枚举enum

枚举类型的声明关键字是enum。大概作用类似联合联合类型,也可以控制变量在一定可选范围内。

只接受枚举类型中的属性对应的值。

enum Type {
    type1 = 'type1',
    type2 = 'type2'
}
let type: Type
type = Type.type1 // 'type1'
type = Type.type2 // 'type2'

如果没有给对象属性赋值,则等于对每个属性以下标开始赋值(下标从0开始,如果给第一个属性赋值一个数字,则会从第一个赋值的数字开始计算下标)。

enum Type {
    type1,
    type2
}

// 等同于
// enum Type {
//     type1 = 0,
//     type2 = 1
// }

let type: Type
type = Type.type1 // 0
type = Type.type2 // 1

四、类class

类的声明关键字是class

class Obj {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

泛型

泛型基本使用

泛型相对而言其他知识点来说,稍微难以理解,所以单独抽出来。

它指的是,在声明类型时无法确定属性的类型,而是在使用时才知道类型的情况。

比如你希望定义一个函数,接受的是什么类型的值就返回什么类型的值,你会怎么做呢?

那么多种类型你难道可以把所有类型的可能性都列出来吗,当然不能,于是就需要用到泛型:

// <T>表示一个可变的类型,在后面就直接拿来使用
let func: { <T>(val: T): T } = (val) => {
    return val
}

使用时,我们可以手动指定T的类型或者ts自动类型推论

func<number>(123) // 手动表示T为number类型
func('123') // 类型推论T为string类型

我们还可以用类型别名type或者接口interface):

type Func = {
    <T>(val: T): T
}
// 或
interface Func {
    <T>(val: T): T
}

let func: Func = (val) => {
    return val
}

或者在函数体中声明类型来声明,与上面是同一种意思:

let func = <T>(val: T): T => {
    return val
}
// 或
function func<T>(val: T): T {
    return val
}

泛型与各声明类型关键字

上述基本使用中提到的内容都是在使用函数时指出T泛型的类型。

我们还有一种情况是,利用接口interface、类型别名type、类class生成不同需求的实例,这时候我们可以把泛型提到接口属性后面,达到在声明对象时改变T类型的目的,听不懂直接看例子:

1. 接口

interface Func<T> {
    (val: T): T
}

// 入参和返回值都为string类型的函数
let func1: Func<string> = (val) => {
    return val
}
// 入参和返回值都为number类型的函数
let func2: Func<number> = (val) => {
    return val
}

func1('123')
func2(123) 

2. 类型别名

与上同理

type Func<T> = {
    (val: T): T
}

// 入参和返回值都为string类型的函数
let func1: Func<string> = (val) => {
    return val
}
// 入参和返回值都为number类型的函数
let func2: Func<number> = (val) => {
    return val
}

func1('123')
func2(123) 

3. 类

类class也有同样的效果

class Obj<T> {
    name: string
    other: T
}

let obj1 = new Obj<string>()
obj1.other = '123'

let obj2 = new Obj<number>()
obj2.other = 123

多个泛型参数

不一定只能指定一个泛型参数,可以同时出现多个,用逗号隔开:

let func = <T, U>(val1: T, val2: U): [T, U] => {
    return [val1, val2]
}

泛型约束

我们使用泛型时会碰到这种问题:

let func = <T>(val: T): string => {
    return val.name // 编译不通过,T作为泛型,没办法确保它含有name属性
}

我们如何能够指出T一定包含name属性呢?

我们可以用到接口继承的方法来约束T

interface HasName {
    name: string
}

let func = <T extends HasName>(val: T): string => {
    return val.name 
}

泛型的默认类型

可以给泛型添加默认类型。

比如实现带有泛型的接口时,如果你不指明泛型类型就会编译不通过。

interface Obj<T> {
    age: T
}

let obj: Obj // 编译不通过,必须指定泛型类型
obj = {age: 123}

可以添加默认类型,这样不指定就会自动以默认类型进行编译:

interface Obj<T = number> {
    age: T
}

let obj: Obj // 等于 let obj: Obj<number>
obj = {age: 123}

工具类型

工具类型内容可以看不懂,会用就行

一、工具类型原理前置内容

1. keyof

获取一个已经声明好的类型所有键值的联合类型

type Obj = {
    name: string,
    age: number
}

type ObjKeys = keyof Obj // "name"|"age"

let key1: ObjKeys = 'name'
let key2: ObjKeys = 'age'

2. typeof

可以以一个已经存在的对象作为类型模板声明一个类型出来:

let obj = {
    name: '小红',
    age: 18
}

type Obj = typeof obj

let obj2: Obj = {
    name: '小明',
    age: 20
}

3. T[key] (索引访问)

类似获取对象的值,获取已存在类型键值对应的值作为新的类型。

type obj = {
    name: string,
    age: number
}

type ObjValue1 = obj["name"] // string
type ObjValue2 = obj["age"] // number
type ObjValue3 = obj["name" | "age"] // string | number

4. & (交叉类型)

&符号将两个声明的类型关联,取他们的并集。

typeinterface声明出来的类型都是可以进行交叉类型的,但是interface更多接触到的是继承

type Obj1 = {
    name: string,
    age: number
}
type Obj2 = {
    name: string,
    isStudent: boolean,
}
let obj: Obj1 & Obj2 = {
    name: '小明',
    age: 20,
    isStudent: true
}

对象中相同键值是不同的类型,交叉之后是never

type Obj1 = {
    param: string,
}
type Obj2 = {
    param: number,
}
let obj: Obj1 & Obj2 // never

联合类型交叉是交集:

type Type1 = number | boolean
type Type2 = string | boolean
let obj: Type1 & Type2 // boolean

5. extends(继承)

在一个已有接口上进行拓展,创造出新的接口。

interface Obj1 {
    name: string,
    age: number
}

interface Obj2 extends Obj1 {
    name: string,
    isStudent: boolean,
}

let obj: Obj2 = {
    name: '小明',
    age: 20,
    isStudent: true
}

6. extends(条件)

extends还可以用来做条件类型判断,类似三元运算。

判断extends前面的类型是否可以分配给后面的类型,这里提到了类型的分配,那我们怎么知道一个类型是否可以分配给另一个类型呢?我单独领出来:类型兼容

// '123'可以分配给'123',所以得到string
type Type = '123' extends '123' ? string : number // type T = string

// '123' | '321'不可以分配给'123',所以得到number
type Type = '123' | '321' extends '123' ? string : number // type Type = number

有一种特殊情况,当extends前面是泛型且传入的是联合类型时,等于依次判断了联合类型中的每个类型是否是可分配给后面的类型。

type Type<T> = T extends '123' ? string : number 

// 依次判断'123'可以分配给'123',所以得到string,
// '321'不能分配给'123'所以得到number,
// 就得到string | number
let test: Type<'123' | '321'> // let test: string | number

如果我们需要限制联合类型的这种依次分配,可以使用[]将泛型包起来。

type Type<T> = [T] extends ['123'] ? string : number

// 判断'123' | '321'是否可分配给'123'
// 不可以,所以得到number
let test: Type<'123' | '321'> // let test: number

7. infer(推断)

找个直接看例子比较好理解,一般用于获取你需要推断出来的类型,infer关键词只能在extends条件类型上使用。

infer U可以肤浅理解为将name对应的类型声明为U,可以在后面条件类型中进行使用。

type Type<T> = T extends { param: infer U } ? U : never

let test1: Type<{ param: number }> // number
let test2: Type<{ param: string }> // string

infer会涉及到协变与逆变的问题,不能理解可以先跳过或者记结论。

当infer处于协变的位置,得到的结果是联合类型

type Type<T> = T extends {
    param1: infer U
    param2: infer U
} ? U : never

// 可以这么理解:由于`param1`的`number`类型和`param2`的`string`类型都可以分配给U,只有`string | number`满足条件
let test1: Type<{ param1: number, param2: string }> // string | number

当infer处于逆变的位置(函数入参),得到的结果是交叉类型

type Type<T> = T extends {
    param1: (a: infer U) => void
    param2: (a: infer U) => void
} ? U : never

let test1: Type<{
    param1: (a: string | boolean) => void,
    param2: (a: number | boolean) => void
}> // boolean

二、内置工具类型

1. Partial

原理

type Partial<T> = {
  [P in keyof T]?: T[P];
}

Partial<T>T的属性都变成可选的

type Obj = {
    name: string,
    age: number
}

let obj1: Obj = {name: '小明'} // 没有age,编译不通过

// Partial<Obj>可以理解为{ name?: string, age?: number }
let obj2: Partial<Obj> = {name: '小明'}  // 编译通过

2. Readonly

原理

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

Readonly<T>T的属性都变成只读的,涉及到知识点只读参数,用法同上,不赘述了。

3. Pick

原理

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Pick<T, K extends keyof T>从类型中的某些属性挑选部分出来生成新属性

type Obj = {
    name: string,
    age: number
}

let obj: Pick<Obj, 'name'>  // { name: string }

4. Record

原理

type Record<K extends keyof any, T> = {
  [P in K]: T
}

构造一个对象类型,键值是前面的联合类型,值统一为后面的类型。

注意前面联合类型中的类型只能是 string | number | symbol,因为键值只能是这些类型。

let obj: Record<'name' | 'other', string>
// 等同于
// let obj: {
//     name: string,
//     other: string
// }

5. Exclude

原理

type Exclude<T, U> = T extends U ? never : T;

用于筛出存在于T但是不存在于U的类型

type Type = Exclude<'123' | '321' | '1234567', '123'>  // '321' | '1234567'

6. Extract

原理

type Extract<T, U> = T extends U ? T : never;

Exclude相反类比,筛选出,存在于T且存在于U的类型,即交集

7. Omit

原理

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

Pick相反类比,从T类型中,剔除K键值形成新的类型。

type Type = {
    name: string,
    age: number
}

let test: Omit<Type, 'age'> // { name: string }

8. Parameters

原理

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

获取函数的参数类型,并放入元组

type ParamsType = Parameters<(name: string, age: number) => void> /// [string,number]
let params: ParamsType = ['小明', 20]

9. ReturnType

原理

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

Parameters类比,只是变成了获取函数返回值的类型。

type Type = ReturnType<(name: string, age: number) => void> // void

10. ConstructorParameters

原理

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

获取类的构造函数入参的类型

class Type {
    constructor(name: string, age: number) {}
}

let type: ConstructorParameters<typeof Type> // [name:string, age:number]

字符串控制

type Type1 = Uppercase<'abc'> // 'ABC' 大写
type Type2 = Lowercase<'ABC'> // 'abc' 小写
type Type3 = Capitalize<'abc'> // 'Abc' 首字母大写
type Type4 = Uncapitalize<'ABC'> // 'aBC' 首字母小写

tips

联合类型

表示可接受由 | 隔开的每个类型。

let a: number | string
a = 123
a = '123'

可选参数

ts的一种语法,对声明具体对象类型函数类型时,可以对某些参数进行可选控制,使用?符号,即允许不存在:

let obj: {
    name: string,
    age?: number
}

obj = {
    name: '小明' // age允许不存在
}
let func = (a: string, b?: number) => {
    return a + b
}

func('123') // 第二个入参允许不存在

只读参数

ts的一种语法,声明具体对象类型时,可以对某些参数进行只读控制,声明之后不允许改变

let obj: {
    readonly name: string,
    age: number
}

obj = {
    name: '小明',
    age: 20,
}

obj.age = 21 // 允许修改
obj.name = '小红' // 编译不通过,只读属性不允许修改

类型推论

如果在声明一个变量时,没有声明类型也没有赋值,则默认类型是any

let a
// 等于 let a: any

a = '123'

如果在声明一个变量时,没有声明它的类型而是直接赋值,则会默认变量的类型为本次赋值的类型。

let a = 123
// 等同于 let a: number = 123

a = '123' //错误

类型断言

很多时候根据ts根据官方自带的类型来校验数据的准确性。

有时候比如你拿到一个any类型的变量,给它赋值了字符串。

但是ts语法不知道,它只会觉得该变量是any类型,所以即便你将它赋值给number类型的变量,也允许你通过。

let str: any = '123'
let strLen: number = str // 编译通过,但是不合理

此时你能够确定它的类型是字符串,就可以使用类型断言(两种方式as或者<>),来标识该变量,让ts语法检查能够合理:

let str: any = '123'

// 使用as来表示断言
let strLen: number = str as string // 编译不通过,string类型无法赋值给number

// 使用<>来表示断言
let strLen: number = <string>str // 编译不通过,string类型无法赋值给number

确定赋值断言

ts中,没有给属性赋值就使用就会编译不通过。

let num: number

console.log(num) // 编译不通过,Variable 'num' is used before being assigned.

我们可以确定该属性会被赋值,添加确定赋值断言 ! 允许编译通过。

let num!: number

console.log(num) // 编译通过,undefined

索引签名

声明具体对象类型时,有时候对象中除了一些我们已经确定了的属性,还有一些另外的未确定的属性,我们也得表示出来:

let obj: {
    name: string,
    age: number
}

obj = {
    name: '小明',
    age: 20,
    interest: '篮球' // 没有声明的属性,编译不通过
} 

可以用索引签名解决:

let obj: {
    name: string,
    age: number,
    [prop: string]: any // 可以接受任意数量的(以字符串类型为键,以any类型为值)的键值对
}

obj = {
    name: '小明',
    age: 20,
    interest: '篮球',
    career: '程序员'
}

合并声明

interface可以将重复声明的同一个类型进行合并,type不可以噢:

interface Obj {
    name: string,
    age: number
}

interface Obj {
    name: string,
    isStudent: boolean,
}

let obj: Obj = {
    name: '小明',
    age: 20,
    isStudent: true
}

类型兼容

不同类型,子类型可以分配给父类型,子类型更加具体,父类型更加笼统,为了更好的理解这几句话,我准备了几个例子。

下列例子中,我会使用确定赋值断言来忽略未赋值使用的问题,我们可以更好的理解类型的兼容。

例子1

各种基本类型可以赋值给any,因为any更加笼统。
不过因为any的特殊性,反之也成立,但是不能确保类型安全,这就是为什么建议避免使用any的原因。

let test1!: number
let test2!: any

test2 = test1

例子2

联合类型,更具体的(类型可能性更少)可以赋值给更笼统的(类型可能性更多),反之不可以。

let test1!: number | string | boolean
let test2!: number | string

test1 = test2 // 可赋值
test2 = test1 // 不可赋值

例子3

继承而来的子接口类型可以赋值给父接口类型,反之不可以。

interface People {
    name: string
}

interface Student extends People {
    subject: string
}

let people!: People
let student!: Student

people = student // 可赋值
student = people // 不可赋值

协变

具有父子关系的类型,经历了构造改变之后,仍然具有父子关系,比如类型数组

interface People {
    name: string
}

interface Student extends People {
    subject: string
}

let people!: Array<People>
let student!: Array<Student>

people = student // 可赋值
student = people // 不可赋值

逆变

具有父子关系的类型,经历了构造改变之后,父子关系调换,比如变成函数入参

当然默认的ts语法是不会报逆变的错误的,你需要在tsconfig.json添加"strictFunctionTypes": true,才可以进行逆变的试验。

interface People {
    name: string
}

interface Student extends People {
    subject: string
}

let people!: { (arg: People): void }
let student!: { (arg: Student): void }

people = student // 不可赋值
student = people // 可赋值

逆变可能不太好理解,你可以想象一下,

假如在student实现函数中,使用了Student类型的特有属性subject,而people的入参对象People类型中是没有subject属性的,

所以你将student赋值给people就会出现问题。

相反,则类型安全。

尾言

有些语言是自己的理解,如果有任何错误或者建议,欢迎指出,我会及时修改。

如果文章对你有帮助的话,欢迎点赞收藏~

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在下月亮有何贵干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值