TypeScript|青训笔记

80 阅读18分钟

这是我参与 [第五届青训营] 笔记创作活动的第四天

为什么选择TypeScript

JS是一门动态的、弱类型语言,TS是一门静态的、弱类型语言。

TS的静态类型的优点:

  • 可读性增强:基于语法解析TSDoc,ide增强
  • 可维护性增强:在编译阶段暴露大部分错误
  • 在大型项目中有更好的稳定性和开发效率

TS是JS的超集:包含于兼容所有的JS特性,支持共存;支持渐进式引入与升级

基本语法

基础数据类型

1、类型声明是TS非常重要的一个特点

2、通过类型声明可以指定TS中变量(参数、形参)的类型

3、指定类型后,当为变量赋值时,TS编译器会自动检查值是否符合类型声明,符合则赋值,否则报错

4、类型声明给变量设置了类型,使得变量之恶能存储某种类型的值

注意:

  • 1、当对变量的声明和赋值时同时进行的,TS编译器会自动判断变量的类型

  • 2、如果你的变量的声明是同时进行的,可以省略掉类型声明

类型:

类型例子描述
number1,2,3任意数字
string"hello",'world'任意字符串
booleantrue,false布尔值true或false
字面量其本身限制变量的值就是该字面量的值
any*任意类型
unknown*类型安全的any
void空值(undefined)没有值(或undefined)
never没有值不能是任何值
object{name:'张三'}任意的JS对象
array[1,2,3]任意JS数组
tuple[4,5]元组,TS新增类型,固定长度数组
enumenum{A,B}枚举,TS中新增类型

语法:

let 变量: 类型;
let 变量: 类型 = 值;
function fn(参数:类型,参数:类型):类型{
    ...
}

示例代码:

// 字符串
const q: string = 'string';
// 数字
const w:number = 1;
// 布尔值
const e:boolean = true;
// null
const r:null = null;
// undefined
const t: undefined = undefined;

对象类型

通过接口来定义对象中的数据类型

// 对象类型

const bytedancer: IBytedancer = {
    jobId:1111,
    name:'liu',
    sex:'woman',
    age:20,
    hobby:'swimming',
}

interface IBytedancer{
    // 只读属性:约束属性不可在对象初始化外赋值
    readonly jobId:number;
    name:String;
    sex:'man' | 'woman' | 'other';
    age:number;
    // 可选属性:定义该属性可以不存在
    hobby?:string;
    // 任意属性:约束所有对象属性都必须是该属性的子类型
    [key:string]: any;
}

函数类型

为函数声明类型的方式有两种:

  • 直接在函数上进行类型补充
  • 给函数变量赋值一个函数的类型声明
// 给函数变量赋值一个函数的类型声明
function add(x,y){
    return x+y;
}

interface IMult{
    (x:number,y:number):number;
}
const mult: IMult = (x,y) => x*y;

// 直接在函数上进行类型补充
function add2(x:number,y:number):number{
    return x+y;
}
const mult2:(x: number,y:number) => number = (x,y) =>x*y;

函数重载

// 函数重载
// 对getDate函数进行重载,timestamp为可缺省参数
function getDate(type:'string',timestamp ?: string):string;
interface IGetDate{
    (type:'string',timestamp?:string):string;
    (type:'date',timestamp?:string):Date;
    (type:'string'|'date', timestamp?:string):Date | string;
}
// 不能将类型"(type: any,timestamp:any) => string | Date"分配给类型 "IGetDate"
// 不能将类型 "string | Date"分配给类型 "string"
// 不能将类型 "Date"分配给类型 "string"
const getDate2: IGetDate = (type,timestamp)=>{
    const date = new Date(timestamp);
    return type === 'string' ? date.toLocaleString() : date;

}

数组类型

数组的类型声明:

类型[ ];

数组<类型>

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

// 类型 + 方括号表示
type IArr1 = number[];
// 泛型表示
type IArr2 = Array<string | number | Record<string, number>>;
// 元组表示
type IArr3 = [number,number,string,string];
// 接口表示
interface IArr4 {
    [key: number]:any;
}

Typescript泛型

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

//创建泛型函数
function id<Type>(value:Type):Type{return value}
//调用泛型函数
const num = id<number>(10)

解释:1、语法:在函数名称的后面添加<>(尖括号),尖括号中添加类型变量,比如此处的Type

2、类型变量Type,是一种特殊类型的变量,它处理类型而不是值

3、该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)

4、因为Type是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型

5、类型变量Type,可以是任意合法的变量名称

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

  • 泛型接口&多泛型
interface IX<T,U>{
    key: T;
    val: U;
}
  • 泛型类
class IMan<T>{
    instance: T;
}
  • 泛型别名
type ITypeArr<T> = Array<T>;

示例:

// 泛型
function getRepeatArr(target){
    return new Array(100).fill(target);
}
type IGetRepeatArr = (target: any)=> any[];
// 不预先指定具体类型,而在使用的时候再指定类型的一种特性
type IGetRepeatArrR = <T>(target: T)=> T[];

类型别名 & 类型断言

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

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

type myType = 1|2|3|4|5|6
let k:myType

解释:1、使用type关键字来创建类型别名

2、类型别名(比如,此处的CustomArray),可以是任何合法的变量名称

3、创建类型别名后,直接使用该类型别名作为变量的类型注解即可

高级类型

联合/交叉类型

  • 联合类型:A|B 联合类型表示一个值可以是几种类型之一
  • 交叉类型:A&B 多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

类型保护与类型守卫

类型保护:访问联合类型时,处于程序安全,仅能访问联合类型中的交集部分

类型守卫:定义一个函数,它的返回值是一个类型谓词,生效范围为子作用域

class类

TS全面支持ES2015中引入的class关键字,并为其添加了类型注解和其他语法

class的基本使用:

class Person{}
const p = new Person()

解释:1、根据TS中的类型推论,可以直到Person类的实例对象p的类型是Person

2、TS中的class,不仅提供了class的语法功能,也作为一种类型存在

类的构造函数:

class Person{
    age:number
    gender:string
    
    constructor(age:number,gender:string){
        this.age = age
        this.gender = gender
    }
}

解释:1、成员初始化后,才可以通过this.age来访问实例成员

2、需要为构造函数指定类型注解,否则会被隐式推断为any;构造函数不需要返回值类型

类的实例方法:

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

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

类的继承:

1、extends(继承父类)

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

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

解释:1、通过extends关键字实现继承

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

2、implements(实现接口)

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

解释:1、通过implements关键字让class实现接口

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

类成员的可见性 :

可以使用TS来控制class的方法或属性对于class外的代码是否可见

可见性修饰符包括:1、public(公有的)

//public 表示公开的、公有的,公有成员可以被任何地方访问,默认可见性
class Animal{
    public move(){
        console,log('Moving along!')
    }
}
//解释:1、在类属性或方法前加public关键字,来修饰该属性或方法是公有的
    // 2、因为public是默认可见性,所以可以直接省略

2、protected(受保护的)

//protected 表示受保护的,仅对其声明所在类的子类中(非实例对象)可见
class Animal{
    protected move(){
        console,log('Moving along!')
    }
}
class Dog extends Animal{
    bark(){
        console.loh('wang')
        this.move()
    }
}
//解释:1、在类属性或方法前面添加protected关键字,来修饰该属性或方法是受保护的
//2、在子类的方法内部可以通过this来访问父类中受保护的成员,但是对实例不可见

3、private(私有的)

//private:表示私有的,只在当前类中可见,对实例对象以及子类也是不可见的
class Animal{
    private move(){
        console,log('Moving along!')
    }
    walk(){
        this.move()
    }
}
//解释:1、在类属性或方法前面添加private关键字,来修饰该属性或方法是私有的
//     2、私有的属性或方法只在当前类中可见,对子类和实例对象也都是不可见的

readonly(只读修饰符):

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

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

解释:1、使用readonly关键字修饰该属性是只读的,(只能修饰属性不能修饰方法)

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

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

类型兼容性

两种类型系统: 1 StructuralType System(结构化类型系统)2Nominal Type System(标明类型系统)。TS采用的是结构化类型系统,也叫做duck typing (鸭子类型),类型检查关注的是值所具有的形状。

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

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

解释:1、Point和Point2D是两个名称不同的类

2、变量p的类型被显示标注为Point类型,但是,它的值却是Point2D的实例,并且没有类型错误

3、因为TS是结构化类型系统,只检查Point和Point2D的结构是否相同(属性数量和属性类型)

4、但是,如果在Normal Type System中(比如:c#、java),他们就是不同的类,类型无法兼容

更准确的说法:对于对象类型来说,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

接口兼容性:

interface Point{x:number;y:number}
interface Point2D {x:number;y:number}
let p1:Point
let p2:Point2D = p1
interface Point3D {x:number;y:number; z:number}
let p3:Point3D
p2 = p3
//类和接口也是兼容的
class Point4D{
    x:number
    y:number
    z:number
}
p2 = new Point4D()

函数兼容性:

函数之间兼容性比较复杂,需要考虑:

1、参数个数 ,参数多的兼容参数少的(参数少的可以赋值给参数多的

type F1 = (a:number) => void
type F2 = (a:number,b:number) => void
let f1:F1
let f2:F2 = f1

解释:1、参数少的可以赋值给参数多的,所以f1可以赋值给f2

2、数组forEach方法的第一个参数是回调函数

3、在JS中省略用不到的参数实际上是很常见的,这样的使用方式,促成了TS中函数类型之间的兼容性

4、并且因为回调函数是有类型的,所以,TS会自动推导出参数item、index、array的类型

2、参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)

type F1 = (a:number) => string
type F2 = (a:number) => string
let f1:F1
let f2:F2 = f1
//解释:函数类型F2兼容函数类型F1,因为F
interface Point2D{x:number;y:number}
interface Point3D{x:number;y:number;z:number}
type F2 = (p:Point2D)=>void//相当于有两个参数
type F3 = (p:Point3D)=>void//相当于有三个参数
let f2:F2
let f3:F3 = f2
//解释:1、注意,此处与前面讲到的接口兼容性冲突
//     2、技巧:将对象拆开,把每个属性看做一个个参数,则,参数少的 f2 可以赋值给参数多的f3

3、返回值类型,只关注返回值类型本身即可

type F5 = ()=>string
type F6 = ()=>string
let f5:F5
let f6:F6 = f5
type F7 = ()=>{name:string}
type F8 = ()=>{name:string; age:number}
let f7: F7
let f8: F8
f7 = f8

解释:1、如果返回值类型是原始类型,此时两个类型要相同,比如,F5和F6

2、如果返回值类型是对象类型,此时成员多的可以赋值给成员少的

交叉类型

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

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

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

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

相同点:都可以实现对象类型的组合

不同点:两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同

interface A{
    fn:(value:number) => string
}
interface B extends A {//此处B处会报错(类型不兼容)
    fn:(value:String)=>string
}
​
interface C {
    fn:(value:string)=>string
}
type D = A & C;//此处不会报错
//可以简单理解为:
fn:(value:string | number)=>string

泛型和keyof

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

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

//创建泛型函数:
function id<Type>(value:Type):Type{return value}
​
//调用泛型函数
const num = id<number>(10)

解释:1、语法:在函数名称的后面添加<>(尖括号),尖括号中添加类型变量,比如此处的Type

2、类型变量Type,是一种特殊类型的变量,它处理类型而不是值

3、该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)

4、因为Type是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型

5、类型变量Type,可以是任意合法的变量名称

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

简化调用泛型函数:

function id<Type>(value:Type):Type{
    return value
}
let num = id(10)

解释:1、在调用泛型函数时,可以省略<类型>来简化泛型函数的调用

2、此时,TS内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量Type的类型

3、比如,传入实参10,TS会自动推断出变量num的类型number,并作为Type的类型

泛型约束:

默认情况下,泛型函数的类型变量Type可以代表多个类型,这导致无法访问任何属性。比如,id('a')调用函数时获取参数的长度:

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

解释:Type可以代表任意类型,无法保证一定存在length属性,比如number类型就没有length。此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)

添加泛型约束收缩类型,主要有以下两种方式:

1、指定更具体的类型

function id<Type>(value:Type[]):Type[]{
    console.log(value.length)
    return value
}
//将类型修改为Type类型的数组

2、添加约束

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

解释:1、创建描述约束的接口ILength,该接口要求提供length属性

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}
getProp(person,'name')

解释: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,3,5]}
}

解释:1、在接口名称的后面添加<类型变量>,那么,这个接口就变成了泛型接口

2、接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量

3、使用泛型接口时,需要显式指定具体的类型(比如,此处的idFunc)。

4.下hi,id方法的参数和返回值类型都是number;ids方法的返回值类型是number[]

泛型类:

class也可以配合泛型来使用

例如:react的class组件的基类Component就是泛型类,不同的组件有不同的props和state

interface IState{count:number}
interface Iprops{maxLength:number}
class InputCount extends React.Component<IProps,IState>{
    state:IState = {
        count:0
    }
    render(){
        return <div>{this.props.maxLength}</div>
    }
}
//解释:React.Component泛型类的两个类型变量,分别指定props和state类型

创建泛型类:

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

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

2、此处的add方法,采用的是箭头函数形式

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

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

泛型工具类型:

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

Partial

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

interface Props{
    id:string
    children:number[]
}
type PartialProps = Partial<Props>

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

Readonly

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

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

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

let props:ReadonlyProps = {id:'1',children:[]}
props.id = '2'//无法赋值,因为属性只读

Pick<Type,Keys>

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

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

解释: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表示:这个对象有三个属性分别为a/b/c,属性值的类型都是string[ ]。

索引签名类型和索引查询类型

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

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

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.映射类型是基于索引签名类型的,所以,该语法类似于索引签名类型,也使用了。

  1. Key in PropKeys,表示Key可以是PropKeys联合类型中的任意一个,类似于forin(let k in obj)。
  2. 使用映射类型创建的新对象类型 Type2和类型 Type1结构完全相同。
  3. 注意:映射类型只能在类型别名中使用,不能在接口中使用。

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

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中所有的键名称中任意一个