Typescript | 青训营笔记

149 阅读31分钟

这是我参与「第四届青训营 」笔记创作活动的的第4天

Typescript 如今运用的越来越广泛,在本次青训营中,深刻体会到了 ts 的魅力所在,于是,我详细的整理了一下 Typescript 的知识点。

js 和 ts 的区别

从编程语言的动静来区分,Typescript 属于静态类型的编程语言,JavaScript 属于动态类型的编程语言。

静态类型:编译期做类型检查 ; 动态类型:执行期做类型检查。

代码是先编译后执行。所以 typescript 能比 JavaScript 更早的发现错误。

ts 相比 js 的优势

  • 更早(写代码的同时)发现错误,减少找 Bug ,改 Bug 事件,提升开发效率。
  • 程序中任何位置的代码都有代码提示,随时随地的安全感,增强了开发体验。
  • 强大的类型系统提升了代码的可维护性,使得重构代码更加的容易。
  • TS 类型推断机制,不需要在代码的每个地方都显示标注类型,让你享受优势的同时,尽量降低了成本。

除此之外,Vue3 源码使用 TS 重写,Angular 默认支持 TS ,React 与 TS 完美配合, Typescript 已经成为大中型前端项目的首选编程语言。

搭建typescript学习环境

安装typescript

npm i -g typescript

查看是否安装成功

tsc -v

安装ts-node

npm i -g ts-node

类型注解

let age:number = 18

说明:代码中的 :number 就是类型注解。

作用:为变量添加类型约束。比如,上述代码中,约定变量 age 的类型为number(数值类型)。

解释:约定了什么类型,就只能给变量赋值该类型的值,否则,就会报错。

常用类型

原始类型

let str: string = "jimmy";
let num: number = 24;
let bool: boolean = false;
let u: undefined = undefined;
let n: null = null;
let obj: object = {x: 1};
let big: bigint = 100n;
let sym: symbol = Symbol("me"); 

注意:默认情况下 nullundefined 是所有类型的子类型。就是说你可以吧 nullundefined 赋值给其他类型。

// null和undefined赋值给string
let str:string = "666";
str = null
str= undefined
​
// null和undefined赋值给number
let num:number = 666;
num = null
num= undefined
​
// null和undefined赋值给object
let obj:object ={};
obj = null
obj= undefined
​
// null和undefined赋值给Symbol
let sym: symbol = Symbol("me"); 
sym = null
sym= undefined
​
// null和undefined赋值给boolean
let isDone: boolean = false;
isDone = null
isDone= undefined
​
// null和undefined赋值给bigint
let big: bigint =  100n;
big = null
big= undefined

注意:虽然 numberbright 都表示数字,但是这两个类型不兼容。

let big: bigint =  100n;
let num: number = 6;
big = num;
num = big;

这样会抛出一个类型不兼容的错误。

数组类型

数组类型的两种写法:

let numbers:number[] = [1,3,5]//推荐使用这种写法
let strings:Array<string> = ['a','b','c']

如果数组中既有number类型,又有string类型,可以这样写:

let arr:(number|string)[] = [1,'a',2,'b']

解释:|(竖线) 在 TS 中叫做联合类型(由两个或者多个其他类型组成的类型,表示可以是这些类型中的任意一种)。

注意:添不添加小括号,两者的意思完全不同

// 不添加小括号,表示:arr1既可以是 number 类型,又可以是 string 类型的数组
let arr1:number|string[] = ['a','b']
arr1 = 123

类型别名

类型别名(自定义类型):为任意类型的别名。

使用场景:当统一类型(复杂)被多次使用时,可以通过类型别名,简化该类型的使用。

type CustomArray = (number|string)[]
let arr1:CustomArray = [1,'a',3,'b']
let arr2:CustomArray = ['x','y',6,7]

解释:

  1. 使用 type 关键字来创建类型别名。
  2. 类型别名(比如,此处的 CustomArray ),可以是任意合法的变量名称。
  3. 创建类型别名后,直接 使用该类型别名作为变量等等类型注解 即可。

函数类型

函数的类型实际上指的是:函数参数返回值的类型。

为函数指定类型的两种方式:1.单独指定参数,返回值的类型 2.同时指定参数,返回值的类型。

  • 单独指定参数,返回值的类型

    function add(num1:number,num2:number):number{
        return num1+num2
    }
    
    const add = (num1:number,num2:number):number=>{
        return num1+num2
    }
    
  • 同时指定参数,返回值类型:

    const add:(num1:number,num2:number) => number = (num1,num2) =>{
        return num1+num2
    }
    

    解释:当函数作为表达式时,可以通过类似箭头函数形式的语法来为函数添加类型。

    注意:这种形式只适用于函数表达式。

void类型

如果函数没有返回值,那么,函数返回值类型为:void

function greet(name:string):void{
    console.log('Hello',name)
}

可选参数

使用函数实现某个功能时,参数可以传也可以不传。这种情况下,在给函数参数指定类型时,就用到可选参数了。

function myslice(start?:number,end?:number):void{
    console.log('起始索引',start,'结束索引',end)
}

可选参数:在可传可不传的参数名称后面添加 (问号)。

注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。

默认参数

function myslice(start:number,end:number = 1):void{
    console.log('起始索引',start,'结束索引',end)
}

剩余参数

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}
let a = [];
push(a, 1, 2, 3);

对象类型

在 TS 中对象的类型就是在描述对象的结构(有什么类型的属性和方法)。

let person:{ name:string; age:number; sayHi():void } = {
    name:'jack',
    age:19,
    sayHi(){}
}

解释:

  1. 直接使用 {} 来描述对象结构。属性采用属性名:类型的形式,方法采用方法名():返回值类型的形式。
  2. 如果方法有参数,就在方法名后面的小括号中指定参数类型(比如:greet(name:string):void)
  3. 在一行代码中指定对象的多个属性类型时,使用;(分号)来分隔。
  • 如果一行代码只指定一个属性类型(通过换行来分隔多个属性类型),可以去掉 (分号).

  • 方法的类型也可以使用箭头函数的形式(比如:{ sayHi:()=>void })。

    let person:{ 
        name:string
         age:number
          sayHi:()=>void
        } = {
        name:'jack',
        age:19,
        sayHi(){}
    }
    

可选属性

对象的属性或者方法,也可以是可选的,此时就用到可选属性了。

可选属性的语法与函数可选参数的语法一致,都使用 ? (问号) 来表示。

function my(person:{name:string;age?:number}){
    console.log(person)
}
my({name:"芜湖"})

接口

当一个对象类型被多次使用时,一般会使用接口来描述对象的类型,达到复用的目的。

解释:

  1. 使用 interface 关键字来声明接口
  2. 接口名称(比如,此处的IPerson) ,可以是任意合法的变量名称
  3. 接口声明后,直接使用接口作为变量的类型
  4. 因为每一行只有一个属性类型,因此,属性后面没有 ; (分号)
interface IPerson {
    name:string
    age:number
    sayHi():void
}
​
let person:IPerson = {
    name:'jack',
    age:19,
    sayHi(){}
}

interface(接口)和type(类型别名) 的对比

  • 相同点:都可以给对象指定类型。

  • 不同点:

    • 接口,只能为对象指定类型。
    • 类型别名,不仅可以为对象指定类型,实际上可以为任意类型指定别名
interface IPerson {
    name:string
    age:number
    sayHi():void
}
type IPerson = {
    name:string
    age:number
    sayHi():void
}
type NumStr = number | string

接口继承

如果两个接口之间有相同的属性或方法,可以 将公共的属性或方法抽离出来,通过继承来实现复用。

比如,这两个接口都有x,y两个属性,重复写会显得非常的繁琐。

interface Point2D { x:number;y:number }
interface Point3 { x:number;y:number;z:number }

更好的方法:

interface Point2D { x:number;y:number }
interface Point3 extends Point2D { z:number }
  • 使用 extends (继承) 关键字实现了接口 Point3D 继承 Point2D。
  • 继承后,Point3D 就有了 Point2D 的所有属性和方法(此时,Point3D同时有了x,y,z三个属性)

字面量类型

let str1 = 'hello ts'
const str2 = 'hello ts'

通过 TS 类型推倒机制,可以得到答案:

  1. 变量 str1 的类型为:string
  2. 变量 str2 的类型为:’hello ts‘

解释:

  1. str1 是一个变量(let),它的值可以是任意字符串,所以类型为:string。
  2. str2 是一个常量(const),它的值不能变化只能是’hello ts‘,所以,他的类型为’hello ts‘

注意:此处的 ’hello ts‘ ,就是一个字面量类型。也就是说某个特定的字符串也可以作为 TS 中的类型

除字符串之外,任意的 JS 字面量(比如,对象,数字等)都可以作为类型使用。

let age:18 = 18//true
let age:19 = 19//false

使用模式:字面量类型配合联合类型一起使用。

使用场景:用来 表示一组明确的可选值列表。

比如,在贪吃蛇游戏中,游戏的方向的可选值只能是上,下,左,右中的任意一个。

function changeDirection(direction:'up'|'down'|'left'|'right'){
    console.log(direction)
}

解释:参数 direction 的值只能是 up/down/left/right 中的任意一个。

优势:相比于 string 类型,使用字面量类型更加精确,严谨。

枚举

枚举的功能类似于字面量类型+联合类型组合功能,也可以表示一组明确的可选值

枚举:定义一组命名常量。它描述一个值,该值可以是这些命名常量中的一个。

enum Direction {up,Down,Left,Right}
​
function changeDirection(direction:Direction){
    console.log(direction)
}
​
changeDirection(Direction.up)

解释:

  1. 使用 enum 关键字定义枚举。
  2. 约定枚举名称,枚举中的值以大写字母开头。
  3. 枚举中的多个值通过 , (逗号) 分隔。
  4. 定义好枚举后,直接使用枚举名称作为类型注解。

注意,形参 direction 的类型为枚举 Direction ,那么,实参的值就应该是枚举 Direction 成员的任意一个。

访问枚举成员,直接通过点 . 语法进行访问。

枚举成员的值及数字枚举

当我们将鼠标移到 Direction.up ,可以看到枚举成员 Up 的值为 0。

注意:枚举成员是有值的,默认为:从 0 开始自增的数值

我们把,枚举成员的值为数字的枚举,称为:数字枚举

当然,也可以给枚举中的成员初始化值。

//Down -> 11,Left -> 12,Right ->13
enum Direction {up = 10,Down,Left,Right}
enum Direction {up = 2,Down = 4,Left = 8,Right = 16}

字符串枚举

字符串枚举:枚举成员的值是字符串。

enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT'
}

注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值

枚举的原理

enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT'
}

上面这行代码,实际上在 JS 中会被编译成下面这样代码

var Direction;
(function (Direction) {
    Direction["Up"] = "UP";
    Direction["Down"] = "DOWN";
    Direction["Left"] = "LEFT";
    Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));

any

原则不推荐使用 any 。(失去 TS 类型保护的优势)

首先: any 类型,允许被赋值为任意类型。

let a: any = 666;
a = "Semlinker";
a = false;
a = 66
a = undefined
a = null
a = []
a = {}

其次:在 any 上访问任何属性都是允许的,也允许调用任何方法。

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

注意:有两种隐式具有 any 类型的情况:1. 声明变量不提供类型也不提供默认值 2. 函数参数不加类型

let age
​
function add(x,y){
    return x+y
}
//上面这两种情况,都是 any 类型

元组

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组最重要的特性是可以限制 数组元素的个数和类型 ,它特别适合用来实现多值返回。****

元祖用于保存定长定数据类型的数据

let x: [string, number]; 
// 类型必须匹配且个数必须为2
​
x = ['hello', 10]; // OK 
x = ['hello', 10,10]; // Error 
x = [10, 'hello']; // Error

当我们确切的知道包含多少个元素,以及特定索引对应的类型,建议使用元组。

元组的解构赋值

let employee: [number, string] = [1, "芜湖"];
let [id, username] = employee;
console.log(`id: ${id}`);//id:1
console.log(`username: ${username}`);//username:芜湖

元组中的可选元素

let option:[string,boolean?]

元组类型的剩余参数

type RestTupleType = [number, ...string[]];
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"];
console.log(restTuple[0]);
console.log(restTuple[1]);

只读的元组

const point: readonly [number, number] = [10, 20];

类型推断

在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型

换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写!

发生类型推论的2种常见场景:1.声明变量并初始化时 2.决定函数返回值时。

let age = 18
function add(num1:number,num2:number){
    return num1+num2
}

注意:这两种情况下,类型注解可以省略不写。

推荐:能省略类型注解的地方就省略

类型断言

有时候你会比 TS 更加明确一个值的类型,此时,可以使用类型断言来制定更具体的类型。

// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
​
// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

下面是一个实际的例子:

<a href="http://www.baidu.com" id="link">百度</a>
const alink = document.getElementById('link')

注意:getElementBYId 方法返回值的类型是 HTMLElement ,该类型只包括所有标签公共属性或方法,不包含 a 标签特有的 herf 等属性。

因此,这个类型太宽泛(不具体),无法操作 herfa 标签特有的属性或方法。

解决方式:这种情况下就需要使用类型断言指定更加具体的类型

使用类型断言:

const alink = document.getElementById('link') as HTMLAnchorElement 

解释:

  1. 使用 as 关键字实现类型断言。
  2. 关键字 as 后面的类型是一个更加具体的类型(TMLAnchorElement 是 HTMLElement 的子类型 )。
  3. 通过类型断言,alink 的类型变得更加的具体,这样就可以访问 a 标签特有的属性和方法了。

另一种语法,使用 <> 语法,这种语法形式不常用知道即可。

const alink = <HTMLAnchorElement>document.getElementById('link')

技巧:在浏览器控制台,通过 console.dir() 打印 DOM 元素,在属性列表的最后面,即可看到该元素的类型。

高级类型

class

基本使用

class Person {
    age:number
    // gender:string = '男'
    gender = '男'
}
const p = new Person()

声明成员 age ,类型为 number(没有初始值)

声明成员 gender,并设置初始值,此时,可以省略注解

class Point {
    x = 10
    y = 10
    
    scale(n:number):void{
        this.x*=n
        this.y*=n
    }
}

方法的类型注解(参数和返回值)与函数用法相同

构造函数

class Person {
    age:number
    gender:string
​
    constructor(age:number,gender:string){
        this.age = age
        this.gender = gender
    }
}
​
let p = new Person(18,'男')

成员初始化(比如,age:number)后,才可以通过 this.age 来访问实例成员

需要为构造函数指定类型注解,否则会被隐私推倒为 any ;构造函数不需要返回值类型。

继承

类继承的两种方式:1.extends(继承父类) 2. implements(实现接口)

说明:JS 中只有 extends ,而 implement 是 TS 提供的。

class Animal{
    move(){ concole,log('Moving along!') }
}
class Dog extends Animal{
    bark(){ console.log('汪!') }
}
const dog = new Dog()

通过 extends 关键字实现继承

子类 Dog 继承父类 Animal,则 Dog 的实例对象 dog 就同时具有了父类 Animal 和子类 Dog 的所有属性和方法。

interface Singable {
    sing():void
}
class Person implements Singable{
    sing(){
        console.log('啦啦啦啦啦啦')
    }
}

通过 implements 关键字让 class 实现接口

Person 类实现接口 Singable 意味着,Person 类中必须提供 Singable 接口中指定的所有方法和属性。

可见性修饰符

在 TS 中,可见性修饰符包括:1.public(公有性) 2. protected(受保护的) 3. private(私有的)

  1. public:表示公有的,公开的,公有成员可以被任何地方访问,默认可见性。

    class Animal {
        public move(){
            console.log('Moving along!')
        }
    }
    

    在类属性或方法面前添加 public 关键字,来修饰改属性或方法是公有的。

    因为 public 是默认可见性,所以,可以直接省略。

  2. protected:表示受保护的,仅对其声明所在类和子类中(非实例对象)可见。

    class Animal {
        protected move() { console.log('Moving along!') }
    }
    class Dog extends Animal {
        bark(){
            console.log('汪!')
            this.move()
        }
    }
    

    在类属性或方法前面添加 protected 关键字,来修饰该属性或方法是所保护的

    在子类的方法内部可以通过 this 来访问父类中受保护的成员,但是,对实例不可见

  3. private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的

    class Animal {
        private move(){ console.log('Moving along!') }
        walk(){
            this.move()
        }
    }
    

    在类属性或方法前面添加 private 关键字,来修饰属性或方法是私有的

    私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的

  4. readonly:表示==只读,用来防止在构造函数之外对属性进行赋值。

    class Person {
        readonly age:number = 18
        constructor(age:number){
            this.age = age
        }
    }
    

    使用 readonly 关键字修饰该属性是只读的,注意只能修饰属性不能修饰方法

    注意:属性 age 后面的类型注解(比如,此处的 number) 如果不加,则 age 的类型为 18(字面量类型)。

    接口或者 {} 表示对象类型,也可以使用 readonly

    interface IPerson {
        readonly name:string
    }
    ​
    let obj:IPerson = {
        name:'jack'
    }
    ​
    obj.name = 'rose'//报错,不能修改被readonly修改后的值
    

类型兼容性

对象之间的兼容性

TS 采用的是结构化类型系统类型检查关注的是值所具有的形状

也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。

class Point { x:number;y:number }
class Point2D { x:number;y:number }
​
const p:Point = new Point2D()

解释:

  1. Point 和 Point2D 是两个名称不同的类
  2. 变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并没有类型错误。
  3. 因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有 x 和 y 两个属性,属性类型也相同)

对于对象类型来说,y 的成员至少与 x 相同,则 x 兼容 y (成员多的可以赋值给成员少的)

class Point { x:number;y:number }
class Point3D { x:number;y:number;z:number }
​
const p:Point = new Point3D()

解释:

  1. Point3D 的成员至少与 Point 相同,则 Point 兼容 Point3D。
  2. 所以,成员多的 Point3D 可以赋值给成员少的 Point。

接口之间的兼容性

接口之间的兼容性,类似于 class 。并且,classinterface 之间也可以兼容。

interface Point { x:number;y:number }
interface Point2D { x:number;y:number }
interface Point3D { x:number;y:number;z:number }
​
let p1: Point = {x:0,y:1}
let p2: Point2D = {x:1,y:2} 
let p3: Point3D = {x:2,y:3,z:4}
​
p1 = p2
p2 = p1
p2 = p3
class Point3D { x:number;y:number;z:number }
let p3:Point2D = new Point3D()

函数之间的兼容性

函数之间兼容性比较复杂,需要考虑:1.参数个数 2.参数类型 3.返回值类型

  1. 参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)
type F1 = (a:number) => void
type F2 = (a:number,b:number) => voidlet f1:F1 = (a:number):void =>{
    console.log('F1')
}
let f2:F2 =(a:number,b:number):void =>{
    console.log('F2')
}
​
f2 =f1
const arr = ['a','b','c']
arr.forEach(()=>>{})
arr.forEach((item)=>>{})

解释:

  1. 参数少的可以赋值给参数多的,所以,f1 可以赋值给 f2
  2. 数组 forEach 方法的第一个参数是回调函数,该示例中类型为: (value:string,index:number,array:string[])=>{}
  3. 在 JS 中省略用不到的函数参数实际上是非常常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性。
  1. 参数类型,相同位置的参数类型要相同 (原始类型) 或 兼容 (对象类型)
type F1 = (a:number) => void
type F2 = (a:number) => voidlet f1:F1 = (a:number):void =>{
    console.log('F1')
}
let f2:F2 =(a:number):void =>{
    console.log('F2')
}
​
f2 =f1

解释:函数类型 F2 兼容函数类型 F1,因为 F1 和 F2 的第一个参数类型相同。

如果参数是一个对象,将对象拆开,把每个属性看做一个个参数,参数少的可以赋值给参数多的。

  1. 返回值类型
type F1 = (a:number) => void
type F2 = (a:number) => voidlet f1:F1 = (a:number):void =>{
    console.log('F1')
}
let f2:F2 =(a:number):void =>{
    console.log('F2')
}
​
f2 =f1

如果返回值类型是原始类型,此时两个类型要相同,比如,上面两个(返回值都是void)

type F1 = () => { name:string }
type F2 = () => { name:string;age:number }

如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如上面的例子,可以将F2的函数 赋值给 F1的函数

交叉类型(&)

交叉类型的功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)。

interface Person { name:string }
interface Contact { phone:string }
type PersonDetail = Person & Contact
let obj:PersonDetail = {
    name:"jack",
    phone:"1264"
}

解释:使用交叉类型后,新的类型 PersonDetail 就同时具备了 Person 和 Contact 的所有属性类型。

相当于:

type PersonDetail = { name:string;phone:string }

交叉类型(&) 和 接口继承(extends) 的对比:

  • 相同点:都可以实现对象类型的组合
  • 不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同。
interface A{
    fn:(value:number) => string
}
​
interface B extends A{
    fn:(value:string) => string
}

此时这里的接口继承会报错,类型不兼容。

interface A {
    fn:(value:number) => string
}
​
interface B {
    fn:(value:string) => string
}
​
type C = A & B

此时就不会报错,相当于:

fn:(value:string|number) => string

泛型

基本使用

泛型是可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用,常用于:函数,接口,class中。

前提需求

创建一个 id 函数,传入什么数据就返回改数据本身(也就是说,参数和返回值类型相同)。

function id(value:number):number { return value }

比如,id(10) 调用以上函数就会直接返回 10 本身。但是,该函数值接收数值类型,无法用于其他类型。

为了能让函数能够接受任意类型,可以将参数类型修改为 any 。但是,这样就失去了 TS 的类型保护,类型不安全。

function id(value:any):any { return value }

泛型在保证类型安全(不丢失类型信息)的同时,可以让函数等于多种不同的类型一起工作,灵活可复用。

创建泛型函数

function id<Type>(value:Type):Type { return value }

解释:

  1. 语法:在函数名称的后面添加 <> (尖括号)中添加类型变量,比如此处的 Type.
  2. 类型变量 Type ,是一种特殊类的变量,它处理的类型不是值。
  3. 该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数的时指定)。
  4. 因为 Type 是类型,因此可以将其作为参数和返回值的类型,表示参数和返回值具有相同的类型。
  5. 类型变量 Type ,可以是雷伊合法类型的变量名称。

调用泛型函数

const num = id<number>(10)
const str = id<string>('a')

解释:

  1. 语法:在函数名后面添加 <> (尖括号) ,尖括号中指定具体的类型,比如,此处的 number
  2. 当传入类型 number 后,这个类型就会被函数声明时指定的类型变量 Type 捕获到。
  3. 此时,Type 的类型就是 number ,所以,函数 id 参数和返回值的类型也都是 number。

同样,如果传入的类型 string ,函数 id 参数和返回值的类型都是 string

这样,通过泛型就做到了让id函数与多种不同类型一起工作,实现了复用的同时保证了类型安全。

简化调用泛型函数

let num = id<number>(10)

上面的函数调用其实可以简化为下面的形式:

let num = id(10)

解释:

  1. 在调用泛型函数时,可以省略<类型> 来简化泛型函数的调用。
  2. 此时,TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出变量 Type 的类型。
  3. 比如,传入实参 10 ,TS 会自动的推断出变量 num 的类型 number ,并作为 Type 的类型。

虽然使用这种简化地方方式调用泛型函数,使代码更短,更易于阅读。但是,当编译器无法判断类型或者推断的类型不准确时,就需要显示地传入类型参数。

泛型约束

默认情况下,泛型函数的类型类型变量 Type 可以代表多个类型,这导致无法访问任何属性。

function id<Type>(value:Type):Type{
    console.log(value.length)
    return value
}

比如说,上面这个例子,是错误的

Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length。

此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)。

interface ILength { length:number }
function id<Type extends ILength>(value:Type):Type{
    console.log(value.length)
    return value
}

解释:

  1. 创建描述约束的接口 ILength,该接口要求提供 ILength 属性。
  2. 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。
  3. 该约束表示:传入的类型必须有 length 属性。

注意:传入的实参 (比如,数组) 只要有 length 属性即可,这也符合前面讲到的接口类型兼容原则。

泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如,第二个类型变量受第一个类型变量约束)

比如,创建一个函数来获取对象中属性的值:

function getProp<Type,Key extends keyof Type>(obj:Type,key:Key){
    return obj[key]
}
​
let person = { name:'jack',age:18 }
let a = getProp(person,'name')
console.log(a)

解释:

  1. 添加了第二个类型变量 Key,两个类型变量之间使用 , 逗号分隔。
  2. keyof 关键字 接受一个对象类型,生成其键名称 (可能是字符串或数字) 的联合类型。
  3. 本实例中 keyof Type 实际上获取的是 person 对象所有键的联合类型,也就是:'name'|'age'
  4. 类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性。

泛型接口

接口也可以配合泛型来使用,以增加其灵活性,增强其复用性。

interface IdFunc<Type>{
    id:(value:Type) => Type
    ids:()=>Type[]
}
​
let obj:IdFunc<number> = {
    id(value) { return value },
    ids(){ return [1,2,3,4] }
}

解释:

  1. 在接口名称后面添加 <类型变量> ,那么,这个接口就变成了泛型接口。
  2. 接口的类型变量,对接口中使用其他成员可见,也就是接口中所有成员都可以使用类型变量。
  3. 使用泛型接口时,需要显示指定具体的类型(比如,此处的IdFunc)
  4. 此时,id 方法的参数和返回值都是 number,ids 方法的返回值类型是 number[]

泛型类

创建泛型类

class GenericNumber<NumType>{
    defaultValue: NumType
    add:(x:NumType,y:NumType)=>NumType
}

类似于泛型接口,在 class 名称后面添加 <类型变量> ,这个类就变成了泛型类。

使用泛型类

const myNum = new GenericNumber<number>()
myNum.defaultValue = 10

类似于泛型接口,在创建 class 实例时,在类名后面通过 <类型> 来指定明确的类型。

泛型工具类型

TS 内置了一些常用的工具类型,来简化 TS 中的一些常见操作。

说明:它们都是基于泛型实现的(泛型适用于多种类型,更加通用),并且是内置的,可以直接在代码中使用。

Partial

用来构造(创建)一个类型,将 Type 的所有属性设置为可选。

interface Props{
    id:string
    children:number[]
}
type PatialProps = Partial<Props>
// 这里面的两个参数必须写,不写会报错
let p1:Props = {
    id:'',
    children:[1]
}
// 这里面的参数可写可不写
let p2:PatialProps = {}

构造出来的新类型 PatialProps 结构 和 Props 相同,但所有属性都变为可选的。

Readonly

用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)

interface Props {
    id:string
    children:number[]
}
type ReadonlyProps = Readonly<Props>

构造出来的新类型 ReadonlyProps 结构和 Props 相同,但所有属性都变为只读的。

let props:ReadonlyProps = { id:'1',children:[] }
// 报错,不能修改
// props.id = '2'

当我们想要重新给 id 属性赋值时,就会报错:无法分配到“id”,因为它是只读属性。

Pick<Type,Keys>

从 Type 中选择一组属性来构造新类型。

interface Props{
    id:string
    title:string
    children:number[]
}
type PickProps = Pick<Props,'id'|'title'>
​
const a:PickProps = {
    id:'111',
    title:'222'
}

解释:

  1. Pick 工具类型由两个类型变量:1.表示选择谁的属性2.表示选择哪几个属性。
  2. 其中第二个类型变量,如果只选择一个则只传入该属性名即可。
  3. 第二个类型变量传入的属性只能是第一个类型变量中存在的属性。
  4. 构造出来的新类型 PickProps,只有 id 和 title 两个属性类型。

Record<Keys,Type>

构造一个对象类型,属性为 Keys ,属性类型为 Type。

type RecordObj = Record<'a'|'b'|'c',string[]>
​
let obj:RecordObj = {
    a:['1'],
    b:['2'],
    c:['3']
}

解释:

  1. Record 工具类型由连个类型变量:1. 表示对象有哪些属性2. 表示对象属性的类型。
  2. 构建的新对象类型 RecordObj 表示:这个对象有三个属性,属性值的类型都是 string[]
// 如果不像上述的那样,就需要
type RecordObj = {
    a:string[],
    b:string[],
    c:string[]
}

更多的泛型类工具请参考:2021 typescript史上最强学习入门文章(2w字) - 掘金 (juejin.cn)

索引类型签名

绝大多数情况下,我们都可以在使用对象之前就确定对象的结构,并为对象添加准确的类型。

使用场景:当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时,就要用到索引签名类型了

interface AnyObject {
    [key:string]:number
}
​
let obj:AnyObject = {
    a:1,
    b:2
}

解释:

  1. 使用 [key:string] 来约束该接口中允许出现的属性名称,表示只要是 string 类型的属性名称,都可以出现在对象中
  2. 对象 obj 中就可以出现任意多个属性(比如,a,b等)。
  3. key只是一个占位符,可以换成任意合法的变量名称。
  4. 隐藏的前置知识:js 中对象 ({ }) 的键是 string 类型的。

映射类型

基于旧类型创建新类型(对象类型),减少重复,提高开发效率。

比如,类型 PropKeys 有 x/y/z,另一个类型 Type1 中也有 x/y/z,并且 Type1 中 x/y/z 的类型相同:

type PropKeys = 'x'|'y'|'z'
type Type1 = { x:number;y:number;z:number }

这样书写,没错,但 x/y/z 重复书写了两次。想这样的情况,就可以使用映射类型来进行简化。

type PropKeys = 'x'|'y'|'z'
type Type2 = { [key in PropKeys]:number }

解释:

  1. 映射类型是基于索引签名类型的,所以,该语法类似于索引q签名类型,也使用了 []
  2. key in PropKeys 表示 key 可以是 Propkeys 联合类型中的任意一个,类似于 forin(let k in obj)
  3. 使用映射类型创建的新对象类型 Type2 和类型 Type1 结构完全相同。
  4. 注意:映射类型只能在类型别名中使用,不能在接口中使用。

keyof

映射类型处理根据联合类型创建新类型外,还可以根据对象类型来创建:

type Props = { a:number;b:string;c:boolean }
type Type3 = { [key in keyof Props]:number }

解释:

  1. 首先,先执行 keyof Props 获取到对象类型 Props 中所有键的联合类型,即 'a'|'b'|'c'
  2. 然后,key in ...就表示 key 可以是 Props 中所有的键名称中的任意一个。

泛型工作类型原理(以partial为例)

type Partial<T> = {
    [P in keyof T]?:T[P]
}
interface Props{
    id:string
    children:number[]
}
type PatialProps = Partial<Props>

索引查询访问类型

上面用到的 T[P] 语法,在 TS 中,叫做 索引查询(访问类型)

作用:用来查询属性的类型

type Props = { a:number;b:string;c:boolean }
type TypeA = Props['a']

Props['a'] 表示查询类型 Props中属性 ‘a’ 对应的类型 number。所以,TypeA 的类型为 number。

注意:[] 中的属性必须存在于被查询类型中,否则会报错。

同时查询多个索引类型

type Props = { a:number;b:string;c:boolean }
type TypeA = props['a'|'b']//string|number
type TypeA = Props[keyof Props]//string|number|boolean

类型声明文件

TS中的两种文件类型

TS 中有两种文件类型:1.ts 文件 2.d.ts文件

  • ts文件

    1. 既包含类型信息又可执行代码。
    2. 可以被编译为.js文件,然后,执行代码。
    3. 用途:编写程序代码的地方。
  • .d.ts文件

    1. 只包含信息类信息的类型声明文件。
    2. 不会生成.js文件,仅用于提供类型信息。
    3. 用途:为 js 提供类型信息。
// .d.ts// 类型
type Props1 = { x:number;y:number }
​
// 可执行代码(会报错!!!)
// function add(num1:number,num2:number):number{
//     return num1+mnum2
// }

.ts 是代码实现文件;.d.ts 是类型声明文件

如果要为 JS 库提供类型信息,要使用 .d.ts 文件。

使用第三方库的类型声明文件

使用已有的类型声明文件有两种:1. 内置类型声明文件 2. 第三方库的类型声明文件。

第三方的库文件有两种存在形式:1.库自带类型声明文件 2.由 DefinitelyTyped 提供。

  1. 库自带类型声明文件:比如,axios

    由于库自带类型声明文件,这种情况下,正常导入该库,TS 就会自动加载库自己的类型声明文件,以提供该库的类型声明。

  2. 由 DefinitelyTyped 提供。

    DefinitelyTyped 是一个 github 仓库,用来提供高质量的 Typescript 类型声明。

    可以通过 npm/yarn 来下载仓库提供提供的 TS 类型声明包。

创建出自己的类型声明文件

项目内共享类型

如果多个 .ts 文件中都用到同一个类型,此时可以创建 .d.ts 文件提供该类型,实现类型共享。

操作步骤:

  1. 创建 index.d.ts 类型声明文件。
  2. 创建需要共享的类型,并使用 export 导出。
  3. 在需要使用共享类型的 .ts 文件中,通过 import 导入即可。

为已有的 JS 文件提供声明

说明:TS 项目中也可以使用. js 文件。

说明:在导入 .js 文件时,TS 会自动加载与 .js 同名的 .d.ts 文件,以提供类型声明。

declare 关键字:用于类型声明,为其他地方(比如,.js文件)已存在的变量声明类型,而不是创建一个新的变量。

declare let count:number

向上面这样,就为变量提供了一个类型声明,函数等也是如此。

tsconfig.json

重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项

{
  "compilerOptions": {
  
    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).
​
    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
​
    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
​
    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。
​
    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
​
    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

参考文献

2021 typescript史上最强学习入门文章(2w字) - 掘金 (juejin.cn)

黑马程序员前端TypeScript教程,TypeScript零基础入门到实战全套教程_哔哩哔哩_bilibili

结语

文章如果有不正确的地方,欢迎指正,共同学习,共同进步。

若有侵权,请联系作者删除。