TypeScript学习笔记(三)

高级类型(上)

交叉类型

交叉类型是将多个类型合并为一个类型,使用符号&进行合并。

实现这样一个函数:将两个对象合并为一个对象并将其返回,那么它的返回值类型就可以是这2个对象类型的合并。

const mergeFunc = <T,U>(arg1:T,arg2:U): T & U =>{
    let res = {} as T & U  // 类型断言
    res = Object.assign(arg1,arg2)  // 把2个对象合并成一个对象
    return res
}
console.log(mergeFunc({a:'a'},{b:'b'}).a); // a

联合类型

联合类型表示一个值可以是几种类型之一。使用符号|分隔每个类型。

实现这样一个函数:接收一个numberstring类型的参数,如果是string类型,则直接返回其长度;如果是number类型,则先转为string类型,再返回其长度。其中参数的类型就可以使用联合类型来表示。

const getLengthFunc = (content:string | number):number =>{
    if(typeof content === 'string') return content.length
    else { return content.toString().length }
}
console.log(getLengthFunc('abcd')); // 4
console.log(getLengthFunc(1234)); // 4

类型保护

有这样一个函数:随机生成0-10的数字,当数字小于5时,返回数字'123',否则返回字符串'abc'。

定义一个变量获取上述函数的返回值,然后根据变量的类型返回字符串的长度或者直接返回数值。

const valueList = [123,'abc']
const getRandomValue = ()=>{
    const number = Math.random()*10
    if(number < 5) { return valueList[0] }
    else { return valueList[1] }
}
const item = getRandomValue()  // 根据类型推论 返回值是 string | number
if((item as string).length){   
    console.log((item as string).length);
}else {
    console.log((item as number).toFixed);
}

可以看到,在ts中为了让最后这段代码正常工作,每处使用到item的地方都需要使用类型断言。

如果能在每个分支里清楚地知道变量item的类型就很方便了,这时需要用到TypeScript里的类型保护

要定义一个类型保护,只要简单地定义一个函数,它的返回值是一个用is生成的类型谓词,即 parameterName is Type这种形式。

自定义的类型保护

使用类型保护,将上面代码进行改写

function isString(value: string | number):value is string{  // 用is指定返回值类型是 value是string类型
    return typeof value === 'string'
}
if(isString(item)){  
    console.log(item.length);
}else {
    console.log(item.toFixed);
}

typeof类型保护

在上面例子可以看到,必须要定义一个函数来判断类型是否是原始类型,有点小题大做。在ts中,typeof value === 'string'会被识别为一个类型保护,这样就可以直接在代码里检查类型了。

if(typeof item === 'string'){
    console.log(item.length);
}else {
    console.log(item.toFixed);
}

typeof类型保护只有两种形式能被识别:typeof value === "typename"typeof value !== "typename",其中"typename"必须是 "number""string""boolean""symbol"

instanceof类型保护

instanceof 用来判断一个实例是不是某个构造函数创建的,或者是不是使用 es6 语法的某个类创建的。在 ts 中,使用 instanceof 可以达到类型保护的效果。

class CreatedByClass1 {
    public age =18
    constructor(){}
}
class CreatedByClass2{
    public name = 'zzz'
    constructor(){}
}
function getRandomItem(){
    return Math.random() < 0.5 ? new CreatedByClass1(): new CreatedByClass2()
}
const item1 = getRandomItem()
if(item1 instanceof CreatedByClass1){
    console.log(item1.age);
}else{
    console.log(item1.name);

}

通过在if分支中使用instanceof判断item的构造函数,如果是由Class1创建的,则会有age属性;如果是由Class2创建的,则会有name属性。

null和undefined

TypeScript具有两种特殊的类型,非严格模式下,nullundefined是任何类型的子类型,可以赋值给任意类型

let values = '123'
values = undefined  //√

如果在tsconfig.js文件中将strictNullChecks为true开启,就不能将null和undefined赋值给除它们自身和void之外的任意类型了。

在这种严格检查的情况下,可以使用联合类型明确地包含它们:

let values:string | null = '123'
values = undefined //error
values = null // right

使用了--strictNullChecks,可选参数和可选属性会被自动地加上 | undefined:

const sumFunc = (x:number,y?:number)=>{
    return x + (y || 0)
}
sumFunc(1,undefined)
sumFunc(1,null) // error 提示 类型“null”的参数不能赋给类型“number | undefined”的参数。

类型保护和类型断言

在联合类型中包含null类型时,如果想要指明null这种情况,可以使用前面的类型保护

const getLengthFunction = (value:string|null):number=>{
    // if(value === null){ return 0 }
    // else { return value.length }
    // 可以简写成
    return (value || '').length
}

使用了--strictNullChecks后,在有一些情况下,编译器不能在声明变量前知道值是否是null的,这时候需要使用类型断言手动指明这个值不是null。

定义一个嵌套函数用于返回拼接后的字符串

// 定义一个函数 返回拼接后的字符串
function getSplicedStr(num:number|null):string{
    function getRes(prefix:string){
        return prefix + num.toFixed().toString() // error 提示 num可能为 "null"
    }
    num = num || 0.1
    return getRes('zzz-')
}

编译器无法去除嵌套函数的null,需要使用类型断言,在不为null的值加!

function getSplicedStr(num:number|null):string{
    function getRes(prefix:string){
        return prefix + num!.toFixed().toString() 
    }
    num = num || 0.1
    return getRes('zzz-')
}
console.log(getSplicedStr(2.03)); // zzz-2

类型别名

类型别名会给一个类型起个新名字,并不是创建了一种新的类型。

type TypeString = string
// let str2:string
// 可以使用TypeString代替string
let str2:TypeString

类型别名也可以使用泛型

type PositionType<T>={ x:T,y:T }
const position1:PositionType<number> = {
    x:1,
    y:-1
}
const position2:PositionType<string> = {
    x:'left',
    y:'top'
}

类型别名可以在属性里引用自己

type Childs<T> = {
    current:T,
    child?:Childs<T>  // 定义了一种树结构
}
let ccc : Childs<string> = {
    current:'first',
    child:{
        current:'second',
        child:{
            current:'third',
            // child:'test',  //这样写会报错,不符合规定
        }
    }
}

类型别名接口类似,有时候能起到相同作用

type Alias = {
    num:number
}
interface Interface {
    num:number
}
let _alias:Alias = {
    num:123
}
let _interface:Interface = {
    num:321
}
_alias = _interface // 类型兼容

类型别名接口的区别在于:类型别名无法被 extends implements,所以类型需要拓展时,需使用接口

如果无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名

字面量类型

字符串字面量

字符串字面量类型 指定字符串必须的固定值。在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。

type Name = 'zzz'
const name1:Name = 'zzz'
const name2:Name = 'haha' // error 提示不能将类型“"haha"”分配给类型“"zzz"”。

type Direction = 'north' | 'east'|'south'|'west'
function getDirectionFirstLetter(direction:Direction){
    return direction.charAt(0)
}
console.log(getDirectionFirstLetter('north'));  // n
console.log(getDirectionFirstLetter('zzz'));  // error 提示 类型“"zzz"”的参数不能赋给类型“Direction”的参数

数字字面量

数字字面量指定类型为一个具体的值。

type Age = 18
interface InfoInterFace{
    name:string,
    age:Age
}
const _info:InfoInterFace = {
    name:'zzz',
    // age:19 // error 提示不能将类型“19”分配给类型“18”
    age:18
}

枚举成员类型

之前在枚举里有介绍过,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

可辨识联合

单例类型(多数指枚举成员类型和字面量类型)、联合类型类型保护类型别名 合并创建一个叫做可辨识联合的高级类型,它也称作标签联合代数数据类型

可辨识联合具有三要素:
1.具有普通的单例类型属性 —— 可辨识的特征
2.一个类型别名包含了哪些类型的联合 ——联合
3.此属性上的类型保护

首先声明将要联合的接口。 每个接口都有 kind属性但有不同的字符串字面量类型。 kind属性称做可辨识的特征标签

interface Square{
    kind:'square',
    size:number
}
interface Rectangle{
    kind:'rectangle',
    height:number,
    width:number
}
interface Circle{
    kind:'circle',
    radius:number
}

使用联合类型类型别名将这三个接口联合到一起

type Shape = Square | Rectangle | Circle

使用可辨识联合

function getArea(s:Shape){
    switch(s.kind){
        case 'square':return s.size * s.size; break;
        case 'rectangle':return s.height*s.width;break;
        case 'circle': return Math.PI * s.radius **2; break;
    }
}

完整性检查

当使用可辨识联合时,如果没有涵盖所有可辨识联合的变化,想让编译器可以提示开发者,这时候就需要完整性检查了。

完整性检查有两种方式可以实现。

1.启用--strictNullChecks并且指定一个返回值类型

function getArea(s:Shape):number{
    switch(s.kind){
        case 'square':return s.size * s.size; break;
        case 'rectangle':return s.height*s.width;break;
        // 如果遗漏一种情况,此时就会报错提示
        // case 'circle': return Math.PI * s.radius **2; break;
    }
}

因为switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined,比如传入第三种类型circle。 但是此时函数的返回值明确为number,因此会报错。这样就知道遗漏了某种情况。然而,这种方法存在些微妙之处且 --strictNullChecks对旧代码支持不好。

2.使用 never类型,编译器用它来进行完整性检查

function assertNever(value:never):never{
    throw new Error('Unexpected object: '+ value)
}
function getArea(s:Shape):number{
    switch(s.kind){
        case 'square':return s.size * s.size; break;
        case 'rectangle':return s.height*s.width;break;
        // 如果遗漏一种情况,此时就会报错提示 类型“Circle”的参数不能赋给类型“never”的参数
        case 'circle': return Math.PI * s.radius **2; break;
        default:return assertNever(s)

    }
}

这里 assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。

高级类型(下)

this类型

ts中,this表示的是某个包含类或接口的子类型。它能很容易的表现连贯接口间的继承

class Counters {
    constructor(public count:number = 0){}
    add(value:number){
        this.count += value;
        return this  // 返回实例
    }
    subtract(value:number){
        this.count -= value
        return this  // 返回实例
    }
}
let counter1 = new Counters(10)
console.log(counter1.add(3).subtract(2)); // 每调用一次方法都会返回实例,可以实现链式调用

由于这个类使用了this类型,继承这个类的子类可以直接使用之前的方法,不需要做任何的改变。

class PowCounter extends Counters{
    constructor(public count:number = 0){
        super(count)
    }
    pow(value:number){
        this.count = this.count ** value
        return this
    }
}
let powCounter = new PowCounter(2)
console.log(powCounter.pow(3).add(1).subtract(3));

如果没有this类型,子类就不能够在继承父类的同时还保持接口的连贯性。ts会对方法返回的this进行判断,如果判断这个this是继承子类创建的实例,那么子类实例调用继承的方法就不会报错了。

索引类型

索引类型查询操作符

索引类型查询操作符keyof连接一个类型,然后返回一个由这个类型所有属性名组成的联合类型

interface InfoInterfaceAdvanced{
    name:string,
    age:number
}
let infoProp:keyof InfoInterfaceAdvanced
infoProp = 'name' // right
infoProp = 'age' // right
// infoProp = 'sex' // error

在非严格模式下,keyof返回类型不为nevernullundefined 类型

interface Type{
    a:never,
    b:never,
    c:string,
    d:number,
    e:undefined,
    f:null,
    g:object,
    h:boolean
}
type Test = Type[keyof Type];// Test代表 string | number | boolean | object

索引访问操作符

索引访问操作符T[K]依赖已经定义的某个类型T,或者在某个泛型T中使用,通过T的属性K得到T[K]类型。

interface InfoInterfaceAdvanced{
    name:string,
    age:number
}
type NameTypes = InfoInterfaceAdvanced['name']  // 访问到接口的InfoInterfaceAdvanced的name字段所对应的string类型

使用索引类型,编译器就能够检查使用了动态属性名的代码。

// T[K][]T的属性值组成的数组 等价于 Array<T[K]>
function getValue<T,K extends keyof T>(obj:T,names:K[]):T[K][]{
    return names.map(n => obj[n])  // 根据属性名返回属性值组成的数组
}
const infoObj = {
    name:'zzz',
    age:18
}
let valuesAge = getValue(infoObj,['age'])
let valuesName = getValue(infoObj,['name'])
let allValues :(string|number)[] = getValue(infoObj,['age','name'])
console.log(valuesAge);  // [18]
console.log(valuesName); // ['zzz']
console.log(allValues); //  [18, 'zzz']

映射类型

基础

TypeScript提供了从旧类型中创建新类型的一种方式 — **映射类型**。 在映射类型里,新类型相同的形式转换旧类型里每个属性。

以下例子是将一个旧类型的所有属性转换成只读的可选属性。

interface Info1{
    age:number,
    name:string,
    sex:string
}
type ReadonlyType<T> = {
    readonly [P in keyof T]?: T[P]  // 遍历每一个属性名
}
type ReadonlyInfo1 = ReadonlyType<Info1> //使用类型别名
let info11:ReadonlyInfo1 = { //对象info11是一个只读属性类型
    age:18,
    name:'zzz',
    //少一个属性也不会报错,因为是可选属性
    //sex:'man'  
}
// 只读属性不可修改
// info11.age = 20 // error,提示age属性是只读属性

映射类型的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

  1. 类型变量 P,它会依次绑定到每个属性。
  2. 字符串字面量联合的 T,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。

typescript中,内置了2种映射类型,可以令每个属性成为 readonly类型或可选的

  • Partial : 令每个属性成为可选的

    type PartialInfo1 = Partial<Info1>
    let partialInfo:PartialInfo1 = {
        age:18
    }
    
  • Readonly: 令每个属性成为 readonly类型

    type ReadonlyInfo1 = Readonly<Info1>
    let info11:ReadonlyInfo1 = {
        age:18,
        name:'zzz',
        sex:'man'
    }
    

typescript中,还有2种内置的映射类型,分别是PickRecord

  • Pick:使用Pick从定义好的类型属性中挑选指定的一组属性,返回一个新的类型定义

    interface Info2{
        name:string,
        age:number,
        address:string
    }
    type nameAndAddress = Pick<Info2,'name'|'address'>
    // 等同于 nameAndAddress = { name:string,address:string }
    const info2:nameAndAddress = {
        name:'zzz',
        address:'beijing'
    }
    console.log(info2); // {name: 'zzz', address: 'beijing'}
    
  • Record:使用Record构造一个具有一组属性 K(类型 T)的类型。即将K中的每个属性,都转为T类型。

    type petsGroup = 'dog'|'cat'|'fish';
    interface PetInfo{
        name:string,
        age:number
    }
    type Pets = Record<petsGroup,PetInfo>
    

    可以看到使用Record<petsGroup,PetInfo>返回了一组属性(来自petsGroup)的属性值类型转为PetInfo类型的类型,即petsGroup的属性值必须是{ name:string,age:number }的形式

    const pets:Pets = {
        dog:{
            name:'wang',
            age:3
        },
        cat:{
            name:'miao',
            age:4
        },
        fish:{
            name:'fish',
            age:2
        }
    }
    

同态 :两个相同类型的代数结构之间的结构保持映射。

这四个内置映射类型,其中ReadonlyPartialPick同态的,而Record不是,它映射出的对象属性值是新的,和输入的属性值是不同的。

由映射类型进行推断

下面是一个例子, T[P]被包装在 Proxy<T>类里

type Proxy<T> = {
    get():T,
    set(value:T):void
}
type Proxify<T> = {
    [ P in keyof T ]:Proxy<T[P]>
}
function proxify<T>(obj:T):Proxify<T>{
    const result = {} as Proxify<T>
    // tslint:disable-next-line:forin
    for(const key in obj){  // 给每个属性添加get和set存取器方法
        result[key] = {
            get:() => obj[key],
            set:(value)=>obj[key] = value
        }
    }
    return result
}
let props = {
    name:'zzz',
    age:18
}
//传入props对象,返回一个新对象,对象的每个属性都具有get和set方法
let proxyProps = proxify(props)
proxyProps.name.set('zzq')
console.log(proxyProps.name.get()); // 'zzq'

上面展示了如何包装一个类型,那么与之相反的就有拆包操作

function unproxify<T>(t:Proxify<T>):T{ 
    const result = {} as T
    // tslint:disable-next-line:forin
    for (const k in t){
        result[k] = t[k].get() //利用每个属性的get方法获取到当前属性值,然后将原本是包含get和set方法的对象改为这个属性值
    }
    return result
}
let originalProps = unproxify(proxyProps)
console.log(originalProps);  // 还原成旧对象props

这种拆包就是根据映射类型进行推断的。

注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

增加或移除特定修饰符

使用+-符号作为前缀来指定增加还是删除修饰符。

  • 使用+
type ReadonlyType<T> = {
    +readonly [P in keyof T]+?: T[P]  // 遍历每一个属性名
}
type ReadonlyInfo1 = ReadonlyType<Info1>
let info11:ReadonlyInfo1 = {
    age:18,
    name:'zzz',
    sex:'man'
}

这个+前缀可以省略

  • 使用-
type ReadonlyType<T> = {
    //类型则表示属性必含而且非只读。
    -readonly [P in keyof T]-?: T[P]  // 遍历每一个属性名
}
type ReadonlyInfo1 = ReadonlyType<Info1>  
let info11:ReadonlyInfo1 = {
    age:18,
    name:'zzz',
    sex:'man'
}
 info11.age = 20 // right 可修改

keyof和映射类型在2.9的升级

TS 在 2.9 版本中,keyof映射类型支持用 numbersymbol 命名的属性

  • keyof
const stringIndex = 'a'
const numberIndex = 1
const symbolIndex = Symbol()
type Objs2 = {
    [stringIndex]:string,
    [numberIndex]:number,
    [symbolIndex]:symbol
}
type keysType = keyof Objs2
let key1: keysType = 2;// error 不能将类型“2”分配给类型“keyof Objs2”
let key2: keysType = 1;//right
let key3: keysType = "b";//error 不能将类型“"b"”分配给类型“keyof Objs2”
let key4: keysType = 'a';//right
let key5: keysType = Symbol();//error 不能将类型“symbol”分配给类型“keyof Objs2”
let key6: keysType = symbolIndex; //right
  • 映射类型
const stringIndex = 'a'
const numberIndex = 1
const symbolIndex = Symbol()

type Objs2 = {
    [stringIndex]:string,
    [numberIndex]:number,
    [symbolIndex]:symbol
}

// 将属性设置为只读属性
type ReadonlyTypes<T> = {
    readonly [P in keyof T]:T[P]
}
let objs3:ReadonlyTypes<Objs2> = {
    a:'aa',
    1:11,
    [symbolIndex]:Symbol()
}
objs3.a = 'bb';  // error,只读属性
objs3[1] = 22;  // error,只读属性
objs3[symbolIndex] = Symbol(); // error 只读属性

元祖和数组上的映射类型

TS 在 3.1 版本中,在元组数组上的映射类型会生成新的元组和数组,不会创建新类型,依旧是元祖类型数组类型,这个类型上会具有 push、pop 等数组方法和数组属性:

// 映射类型
type MapToPromise<T> = {
    [K in keyof T]:Promise<T[K]>
}
// 元祖类型
type Tuple = [number,string,boolean]
// 创建元祖类型上的映射类型
type promiseTuple = MapToPromise<Tuple>
// 当指定变量tuple的类型为promiseTuple后,tuple1是一个元祖类型,它的三个元素类型都是一个Promise,且resolve的参数类型依次为number、string和boolean。
let tuple1:promiseTuple = [
    new Promise((resolve,reject)=>resolve(1)),
    new Promise((resolve,reject)=>resolve('2')),
    new Promise((resolve,reject)=>resolve(false)),
]

unknown类型

unknown类型是ts在3.0版本新增的顶级类型,相对于any是安全的。使用时有以下注意事项

  • ①任何类型都可以赋值给unknown类型

    let value1: unknown
    value1 =  'a'
    value1 = 123
    
  • ②如果没有类型断言或基于控制流的类型细化时,unknown不可以赋值给其他类型。此时它只能赋值给unknown和any类型

    let value2: unknown
    let value3: string = value2 // error 类型“unknown”分配给类型“string”
    value1 = value2
    
  • ③如果没有类型断言或基于控制流的类型细化时,不能在它上面进行任何操作

    let value4: unknown
    value4 += 1 // error 运算符“+=”不能应用于类型“unknown”和“1”。
    
  • ④unknown与任何其他类型组成的交叉类型,最后都等于其他类型

    type type1 = string & unknown   // type1代表string类型
    type type2 = number & unknown   // type2代表number类型
    type type3 = unknown & unknown  // type3代表unknown类型
    type type4 = unknown & string[] // type4代表string[]类型
    
    

    ⑤unknown与任何其他类型(除了any类型)组成的联合类型,都等于unknown类型

    type type5 = unknown | string   // type5代表unknown类型
    type type6 = any | unknown      // type6代表unknown类型
    type type7 = number[] | unknown // type7代表unknown类型
    

    ⑥never类型是unknown的子类型

    type type8 = never extends unknown ? true : false  // type8代表true
    

    ⑦keyof unknown 等于类型never

    type type9 = keyof unknown  // type9代表never类型
    

    ⑧只能对unknown进行等或不等操作,不能进行其他操作

    value1 === value2  // 报的是tslint错误
    value1 !== value2  // 报的是tslint错误
    value1 += value2 // ts报错,unknown不能进行其他操作
    

    ⑨unknown类型的值不能访问它的属性、作为函数调用和作为类创建实例

    let value10: unknown = {
         age:10
     }
     value10.age // error 类型“unknown”上不存在属性“age”。
     
     
     let value10:unknown = (a,b)=>{
         return a + b
     }
     value10(1,2) // error
    

    ⑩使用映射类型,如果遍历的是unknown类型,则不会映射任何属性

    type Types1<T> = {
        [P in keyof T]:number
    }
    type type11 = Types1<any>
    type type12 = Types1<unknown>  // 什么字段都没有
    

条件类型

基础

条件类型 从语法上看像一个三元表达式,它会以一个条件表达式进行类型关系检测,然后在后面两种类型中选择一个,写法如下:

// T extends U ? X : Y  这个表达式的意思是,如果 T 是不是 U 类型的子类型,则是 X 类型,否则是 Y 类型。
//声明一个函数,它的参数接收一个字符串类型,当类型为 true 时返回 string 类型,否则返回 number 类型
type Types2<T> = T extends string ? string : number
let index : Types2<'a'>  // index类型是string
let index2: Types2<false> // 传入非string类型参数,index2类型是number类型

分布式条件类型

当待检测类型是联合类型时,该条件类型就被称为分布式条件类型,在实例化时会自动分发成联合类型。

type TypeName<T> = T extends any ? T : never
type Type3 = TypeName<string | number >  // type3代表 string | number 联合类型

一个分布式条件类型的实际应用:

type Diff<T,U> = T extends U? never: T
type Test2 = Diff<string|number|boolean, undefined|number>  // Test2代表 string | boolean的联合类型

这里定义的条件类型的作用就是判断类型T是否是U类型的子类型,如果是,则返回never类型,否则返回T类型。

这个条件类型已经内置在 TS 中了,叫Exclude

下面是一个条件类型和映射类型结合的例子。

type Type7<T> = {
    // tslint:disable-next-line:ban-types
    [K in keyof T]:T[K] extends Function ? K:never
}[keyof T]
interface Part {
    id: number;
    name:string;
    subparts(newName:string):void;
    undatePart(newName:string):void;
}
type Test1 = Type7<Part> // Test1 代表 "subparts" | "undatePart" 联合类型

这个例子中,接口 Part 有四个字段,其中 updatePart 的值是函数,也就是 Function 类型。Type7的定义中,涉及到映射类型、条件类型、索引类型。首先[K in keyof T]用于遍历 T 的所有属性名,属性值使用了条件类型,T[K]是当前属性名的属性值,T[K] extends Function ? K : never表示如果属性值为 Function 类型,则值为属性名字面量类型,否则为 never 类型。接下来使用[keyof T]返回类型不为never、null、undefined 类型。

条件类型的类型推断-infer

条件类型提供一个infer关键字用来推断类型。

假设定义一个条件类型:如果传入的类型是数组,则返回它元素的类型,如果是一个普通类型,则直接返回这个类型。

  • 不使用infer版本:

    type Type8<T> = T extends any[] ? T[number] : T
    type Test3 = Type8<string[]> //Test3 代表string类型
    type Test4 = Type8<string>   //Test4 代表string类型
    

    如果传入 Type8 的是一个数组类型,那么返回的类型为T[number],也就是该数组的元素类型,如果不是数组,则直接返回这个类型。

  • 使用infer版本

    type Type9<T> = T extends Array<infer U> ? U : T;
    type Test5 = Type9<string[]>  //Test5 代表string类型
    type Test6 = Type9<string>    //Test6 代表string类型
    

    这里 infer 能够推断出 U 的类型,并且供后面使用,可以理解为这里定义了一个变量 U 来接收数组元素的类型。

TS预定义条件类型

TS 在 2.8 版本增加了一些预定义的有条件类型

  • Exclude<T, U>,从 T 中去掉可以赋值给 U 的类型:
type Type10 = Exclude<'a'|'b'|'c','a'|'b'>  // type10 代表 ‘c’
  • Extract<T, U>,选取 T 中可以赋值给 U 的类型:
type Type11 = Extract<'a'|'b'|'c','a'> // Type11 代表a
type Type12 = Extract<'a'|'b'|'c','d'> // Type12 代表never
  • NonNullable,从 T 中去掉 null 、 undefined和never:
type Type13 = NonNullable<string | number | null | undefined | never > // string | number
  • ReturnType,获取函数类型返回值类型:
type Type14 = ReturnType<() => string> // Type14 代表string
type Type15 = ReturnType<() => void>   // 函数没有返回值,Type15代表void
  • InstanceType,获取构造函数类型的实例类型:

InstanceType源码如下:

type InstanceType<T extends new (...args: any[]) => any> = T extends new (
  ...args: any[]
) => infer R
  ? R
  : any;

InstanceType 条件类型要求泛型变量 T 类型是创建实例为 any 类型的构造函数,而它本身则通过判断 T 是否是构造函数类型来确定返回的类型。如果是构造函数,使用 infer 可以自动推断出 R 的类型,即实例类型;否则返回的是 any 类型。

例子:

class AClass {
    constructor(){}
}
type T1 = InstanceType<typeof AClass> // T1 代表AClass
type T2 = InstanceType<any>   // T2 代表any
type T3 = InstanceType<never> // T3 代表never
type T4 = InstanceType<string> // error
type T5 = InstanceType<Function> // error

  • Omit<T, U>,选取 T类型中除了 K类型的所有属性:
interface Todo{
    title:string,
    description:string,
    completed:boolean
}
type TodoPreview = Omit<Todo,"description">  // TodoPreview 代表 { title:string, completed:boolean } 类型
  • Parameters,获得函数的参数类型组成的元组类型:
type P1 = Parameters<() => void> // []
type P2 = Parameters<typeof Array.isArray> // [any]
type P3 = Parameters<typeof parseInt> // [string, number?]
type P4 = Parameters<typeof Math.max> // number[]

模块和命名空间

在ts中,“内部模块”(使用module关键字)现在称做“命名空间”“外部模块”(使用export关键字)现在则简称为“模块”

模块

export

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。

export interface FuncInterface{
    name:string;
    (arg:number):string
}
export class ClassC{
    constructor(){}
}
class ClassD{
    constructor(){}
}
export { ClassD }
// as 对导出的部分重命名
export { ClassD as ClassNamedD }

还可以使用export···from引入别的模块然后再在当前模块重新导出

// b.ts
export const name = 'zzz'
export const age = 18

//a.ts
//导出b模块的全部
export * from './b'
//只导出b模块中的name
export { name } from './b'
//只导出b模块中的name并且重命名为NameProp
export { name as NameProp } from './b'

import

import 可以用来导入其它模块中的导出内容。

①导入一个模块中的某个导出内容

// b.ts
export const name = 'zzz'
export const age = 18

//index.ts
import { name } from './b'
console.log(name);

②将整个模块导入到一个变量,并通过它来访问模块的导出部分

import * as info from './b'
console.log(info.name);

③可以对导入内容重命名

import { name as nameProp } from './b'
console.log(nameProp);

④具有副作用的导入模块

不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:

import './a

export default

export default 是默认导出。每个模块都可以有一个default导出。 默认导出使用 default关键字标记;并且一个模块只能够有一个default导出。 需要使用一种特殊的导入形式来导入 default导出。

//c.ts
export default 'zzz'

//index.ts
import name from './c'
console.log(name);

export = 和 import xx = require()

为了支持CommonJS和AMD的exports, TypeScript提供了export =语法。

export =语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。

若使用export =导出一个模块,则必须使用TypeScript的特定语法import module = require("module")来导入此模块。

//c.ts
const name = 'zzz'
export = name

// index.ts
import name = require('./c')
console.log(name);

应用例子

npm install moment 安装moment库

引入moment库可以有以下几种形式

//1
import moment from 'moment';

//2
import * as moment from 'moment';

//3
import moment = require('moment')

命名空间

“内部模块”现在叫做“命名空间”。现在一般不推荐使用命名空间了,使用较多的是模块。

定义和使用

命名空间的定义实际相当于定义了一个大对象,里面可以定义变量、接口、类、方法等等,但是如果不使用export 关键字指定哪些内容对外可见的话,外部是无法访问的.

使用namespace定义一个命名空间

// space.ts
// 所有涉及内容验证的方法放一起
// 使用namespace 定义一个命名空间
namespace Validation{
    // 用来验证一个值是字母
    const isLetterReg = /^[A-Za-z]+$/
    // 用来验证一个值是数字
    export const isNumberReg = /^[0-9]+$/
    export const checkLetter = (text:any)=>{
        return isLetterReg.test(text)
    }
}

在index.ts中引入命名空间,需要使用三斜杆/// <reference path="" />说明要引入的命名空间

// 使用三斜杆说明要引入的命名空间
/// <reference path="./space.ts" />
let isLetter = Validation.checkLetter('abc')
console.log(isLetter);

可以在终端将输入文件编译为一个输出文件,需要使用--outFile标记:

tsc --outFile src/index.js src/ts-module/index.ts

可以看到在src目录下,生成了index.js,内容如下:

// 所有涉及内容验证的方法放一起
// 使用namespace 定义一个命名空间
var Validation;
(function (Validation) {
    // 用来验证一个值是字母
    var isLetterReg = /^[A-Za-z]+$/;
    // 用来验证一个值是数字
    Validation.isNumberReg = /^[0-9]+$/;
    Validation.checkLetter = function (text) {
        return isLetterReg.test(text);
    };
})(Validation || (Validation = {}));

// 使用三斜杆说明要引入的命名空间
/// <reference path="./space.ts" />
var isLetter = Validation.checkLetter('abc');
console.log(isLetter);

拆分为多个文件

space.ts中的验证拆分为number-validtion.tsletter-validation.ts

//number-validtion.ts
namespace Validation{
    // 用来验证一个值是数字
    export const isNumberReg = /^[0-9]+$/
    export const checkNumber = (text:any)=>{
        return isNumberReg.test(text)
    }
}

//letter-validation.ts
namespace Validation{
    // 用来验证一个值是字母
    export const isLetterReg = /^[A-Za-z]+$/

    export const checkLetter = (text:any)=>{
        return isLetterReg.test(text)
    }
}

在index.ts中引入使用这2个命名空间

// 使用三斜杆说明要引入的命名空间
/// <reference path="./letter-validation.ts" />
/// <reference path="./number-validation.ts" />
let isLetter = Validation.checkLetter('abc')
let isNumber = Validation.checkNumber(123)
console.log(Validation,isLetter,isNumber);

在终端使用命令tsc --outFile src/index.js src/ts-module/index.ts 进行编译

可以看到index.js的内容包含了这2个命名空间,如下:

var Validation;
(function (Validation) {
    // 用来验证一个值是字母
    var isLetterReg = /^[A-Za-z]+$/;
    Validation.checkLetter = function (text) {
        return isLetterReg.test(text);
    };
})(Validation || (Validation = {}));
var Validation;
(function (Validation) {
    // 用来验证一个值是数字
    Validation.isNumberReg = /^[0-9]+$/;
    Validation.checkNumber = function (text) {
        return Validation.isNumberReg.test(text);
    };
})(Validation || (Validation = {}));
// 使用三斜杆说明要引入的命名空间
/// <reference path="./letter-validation.ts" />
/// <reference path="./number-validation.ts" />
var isLetter = Validation.checkLetter('abc');
var isNumber = Validation.checkNumber(123);
console.log(Validation,isLetter,isNumber);

在node环境中运行,结果如下

可以看到定义同名的命名空间,最后都会合并成一个,它们是不会冲突的。

别名

可以使用import关键字给常用的对象起别名,这里的别名不是类型别名,import也不是为了引入模块。

namespace Shapes{
    export namespace Polygons{
        export class Triangle{}
        export class Square{}
    }
}
// 使用别名可以简化深层次嵌套取内容的过程
import polygons = Shapes.Polygons
let triangle = new polygons.Triangle()

模块解析

相对和非相对模块导入

根据模块引用是相对的还是非相对的,模块导入会以不同的方式解析。

相对导入是以/(根目录)./(当前目录)../(上一级目录)开头的。

相对导入在解析时是相对于导入它的文件,并且不能解析为一个外部模块声明。 你应该为你自己写的模块使用相对导入,这样能确保它们在运行时的相对位置。

②所有其它形式的导入被当作非相对的。

非相对模块的导入可以相对于baseUrl或通过路径映射来进行解析。 它们还可以被解析成外部模块声明。 使用非相对路径来导入你的外部依赖。

模块解析策略

ts中有两种模块解析策略,一个是Node,一个是Classic

在tsconfig.json中,可以使用 --moduleResolution标记来指定使用哪种模块解析策略。若未指定,那么在使用了 --module AMD | System | ES2015时的默认值为Class,其它情况时则为Node

①Classic

这种策略在以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。

相对导入的模块是相对于导入它的文件进行解析的。

因此src/ts-module/index.ts 使用import * from './a'引入a模块会使用下面的查找流程:

  1. /root/src/ts-modult/a.ts
  2. /root/src/ts-modult/a.d.ts

对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。

②Node

TypeScript是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript在Node解析逻辑基础上增加了TypeScript源文件的扩展名( .ts.tsx.d.ts)。 同时,TypeScript在 package.json里使用字段"types"来表示类似"main"的意义 - 编译器会使用它来找到要使用的"main"定义文件。

比如,有一个导入语句import { b } from "./moduleB"/root/src/moduleA.ts里,会以下面的流程来定位"./moduleB"

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (如果指定了"types"属性)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

回想一下Node.js先查找moduleB.js文件,然后是合适的package.json,再之后是index.js

类似地,非相对的导入会遵循Node.js的解析逻辑,首先查找文件,然后是合适的文件夹。

模块解析配置项

模块解析配置项都放在tsconfig.json中。详情介绍可以查看文档

①Base URL

在利用AMD模块加载器的应用里使用baseUrl是常见做法,它要求在运行时模块都被放到了一个文件夹里。 这些模块的源码可以在不同的目录下,但是构建脚本会将它们集中到一起。

设置baseUrl来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于 baseUrl

②路径映射

有时模块不是直接放在baseUrl下面。TypeScript编译器通过使用tsconfig.json文件里的"paths"设置路径映射。

下面是一个如何指定 jquery"paths"的例子。

{
  "compilerOptions": {
    "baseUrl": ".", // This must be specified if "paths" is.
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"] // 此处映射是相对于"baseUrl"
    }
  }
}

请注意"paths"是相对于"baseUrl"进行解析。 如果 "baseUrl"被设置成了除"."外的其它值,比如tsconfig.json所在的目录,那么映射必须要做相应的改变。

③rootDirs

可以使用"rootDirs"来告诉编译器。 "rootDirs"指定了一个_roots_列表,列表里的内容会在运行时被合并。

声明合并

补充知识

TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值。 创建命名空间的声明会新建一个命名空间,它包含了用(.)符号来访问时使用的名字。 创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。 最后,创建值的声明会创建在JavaScript输出中看到的值。

Declaration Type Namespace Type Value
Namespace X X
Class X X
Enum X X
Interface X
Type Alias X
Function X
Variable X

合并接口

最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。

定义2个同名接口,每个接口有不同的字段

interface InfoInter{
    name:string
}
interface InfoInter{
    age:number
}

最后,ts会把这2个同名接口合并为一个声明

let infoInter:InfoInter

infoInter = {
    name:'zzz',
    // error,缺少age属性,需要补充age属性
    age:18
}

接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 同时需要注意,当接口 A与后来的接口 A合并时,后面的接口具有更高的优先级。

interface InfoInter{
    name:string;
    getRes(input:string):number
}
interface InfoInter{
    name:string;
    getRes(input:number):string
}
let infoInter:InfoInter

infoInter = {
    name:'zzz',
    getRes(text:any):any{
        if(typeof text === 'string') return text.length
        else return String(text)
    }
}
console.log(infoInter.getRes('123')); // 3

console.log(infoInter.getRes(123)); // '123'

合并命名空间

与接口相似,同名的命名空间也会合并其成员。

namespace Validations{
    export const checkNumber = () => {}
}
namespace Validations{
    export const checkLetter = ()=>{}
}

// 合并为
namespace Validations{
    export const checkNumber = () => {}
    export const checkLetter = ()=>{}
}

非导出成员(没有使用export)仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员。

namespace Validations{
    const numberReg = /^[0-9]+$/
    export const checkNumber = () => {}
}
namespace Validations{
    console.log(numberReg); // error 找不到名称“numberReg”
    export const checkLetter = ()=>{}
}

不同类型合并

命名空间和类

同名的类和命名空间在定义的时候,类的定义必须要在命名空间前面,最后合并之后的结果是一个包含一些与命名空间导出内容为静态属性的类

class Validations {
    constructor(){}
    public checkType(){}
}
namespace Validations {
    export const numberReg = /^[0-9]+$/
}
console.dir(Validations.numberReg); /// ^[0-9]+$/

命名空间和函数

同名的函数和命名空间在定义的时候,函数的定义也必须要在命名空间前面。

function countUp(){
    countUp.count++
}
namespace countUp {
    export let count = 0
}
console.log(countUp.count); // 0
countUp()
console.log(countUp.count); // 1
countUp()
console.log(countUp.count); // 2

命名空间和枚举

同名的函数和命名空间在定义的时候,没有先后顺序之分。

enum Colors {
    red,
    green,
    blue
}
namespace Colors{
    export const yellow = 3
}
console.log(Colors);  // {0: 'red', 1: 'green', 2: 'blue', red: 0, green: 1, blue: 2, yellow: 3}

装饰器

若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

{
    "compilerOptions": {
        "experimentalDecorators": true
    }
}

装饰器定义

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器要紧挨修饰内容的前面,而且所有的装饰器不能用在后缀为.d.ts的文件中和任何外部上下文中。

function helloWord(target: any) {
    console.log('hello Word!');
}

@helloWord
class HelloWordClass {

}

装饰器工厂

装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。

function helloWord(target: any) {
    // tslint:disable-next-line:only-arrow-functions
    return function(target){
        console.log('hello Word!',target);
    }
}

@helloWord('zzz')
class HelloWordClass {

}

装饰器组合

多个装饰器可以同时应用到一个声明上,就像下面的示例:

  • 书写在同一行上:
@f @g x
  • 书写在多行上:
@f
@g
x

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(fg)(x)等同于f(g(x))。

同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值。
  2. 求值的结果会被当作函数,由下至上依次调用。

使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:

function setName(){
    console.log('get setName');
    // tslint:disable-next-line:only-arrow-functions
    return function(target){
        console.log('setName');
    }
}
function setAge(){
    console.log('get setAge');
    // tslint:disable-next-line:only-arrow-functions
    return function(target) {
        console.log('setAge');
    }
}

@setName()
@setAge()
class ClassDec{

}

在控制台里会打印出如下结果:

get setName
get setAge
setAge
setName

装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

let sign = null
function setName(name:string){
    return (target:new()=>any)=>{
        sign = target
        console.log(target.name);
    }
}
@setName('zzz')
class ClassDec{
    constructor(){}
}
console.log(sign === ClassDec); // true
console.log(sign === ClassDec.prototype.constructor); // true

通过装饰器可以修改类的原型对象和构造函数。

function addName(constructor:new()=>any){
    constructor.prototype.name = 'zzz'
}
@addName
class ClassD{}
interface ClassD{  // 定义同名接口做声明合并
    name:string
}
const d = new ClassD()
console.log(d.name);

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。可以使用这个特性,修改类的实现,但是必须注意处理好原来的原型链。

可以通过装饰器覆盖类里面的操作。

定义类装饰器返回一个类,并且这个类继承了被装饰的类Greeter,并替换了被装饰的类的声明

function classDecorator<T extends new(...args:any[]) =>{}>(target:T){
    return class extends target{
        newProperty = 'new property'
        hello = 'override'
    }
}
@classDecorator
class Greeter{
    public property = 'property'
    public hello : string
    constructor(m:string){
        this.hello = m
    }
}
console.log(new Greeter('world'));

可以看到Greeter类的实例不仅包含类中的实例属性还包含装饰器定义的类的实例属性。

修改类装饰器,传入的参数为any类型并且让它返回的类不再继承被修饰的类

function classDecorator(target:any):any{
    return class{
        newProperty = 'new property'
        hello = 'override'
    }
}

可以看到最后创建的实例只包含了装饰器定义的类的实例属性。

方法装饰器

方法装饰器用来处理类中的方法,可以处理方法的属性描述符,可以处理方法的定义。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。

对象可以设置属性,如果属性值是函数,那么这个函数称为方法。每一个属性和方法定义的时候都伴随3个属性描述符

1.configurable 可配置

  1. writeable 可写

  2. enumerable 可枚举

// 如果要修改这3个属性描述符,需要使用ES5的Object.defineProperty方法
interface ObjWithAnyKeys{
    [key:string]:any
}
let obj12:ObjWithAnyKeys = {}
Object.defineProperty(obj12,'name',{
    value:'zzz',
    writable:false,    // 不可写
    configurable:true, // 可配置
    enumerable:true    // 可枚举
})
console.log(obj12.name); // zzz
obj12.name = 'test'
console.log(obj12.name); // 不可写,值还是zzz

注意  如果代码输出目标版本小于ES5,属性描述符将会是undefined

方法装饰器应用例子如下

function enumerable(bool:boolean){
    return (target:any,propertyName:string,descriptor:PropertyDescriptor)=>{
        console.log(target);
        // 设置属性描述符
        descriptor.enumerable = bool
    }
}
class ClassF{
    constructor(public age:number){}
    @enumerable(false)
    getAge(){
        return this.age
    }
}
const classF = new ClassF(18)
// 遍历实例
// tslint:disable-next-line:forin
for(const key in classF){
    console.log(key);  //只输出了age属性,因为继承的getAge方法被设置为不可枚举

}

如果访问器装饰器返回一个值,它会被用作方法的属性描述符。

修改上面的方法装饰器,让它返回一个值

function enumerable(bool:boolean):any{
    return (target:any,propertyName:string,descriptor:PropertyDescriptor)=>{
        return {
            value(){
                return 'not age'
            },
            enumerable:bool
        }
    }
}
class ClassF{
    constructor(public age:number){}
    @enumerable(false)
    getAge(){
        return this.age
    }
}
const classF = new ClassF(18)
console.log(classF.getAge());  // not age

访问器装饰器

访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。

注意  TypeScript不允许同时装饰一个成员的getset访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了getset访问器,而不是分开声明的。

同样地,访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。
function enumerable(bool:boolean){
    return (target:any,propertyName:string,descriptor:PropertyDescriptor)=>{
        descriptor.enumerable = bool
    }
}
class ClassG{
    private _name:string
    constructor(name:string){
        this._name = name
    }
    @enumerable(false)
    get name(){
        return this._name
    }
    set name(name){
        this._name = name
    }
}
const classG = new ClassG('zzz')
// tslint:disable-next-line:forin
for(const key in classG){
    console.log(key);  // 只打印出 _name
}

属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
function printPropertyName(target:any,propertyName:string){
    console.log(propertyName);
}
class ClassH{
    @printPropertyName
    name:string
}

参数装饰器

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。

注意  参数装饰器只能用来监视一个方法的参数是否被传入。

function required(target:any,propertyName:string,index:number){
    console.log(`修饰的是${propertyName}的第${index+1}个参数`);
}
class ClassI{
    public name:string = 'zzz'
    public age:number = 18
    getInfo(prefix:string,@required infoType:string):any{
        return prefix + ' '+this[infoType]
    }
}
interface ClassI{
    // tslint:disable-next-line:ban-types
    [key:string]:string|number|Function
}
const classI = new ClassI()
classI.getInfo('hihi','age')

混入

对象的混入

对象的混入使用ES6的Object.assign方法。

例子:使a对象里面混入一个对象b让a里面有对象b的属性

interface ObjectA{
    a:string
}
interface ObjectB{
    b:string
}
let Aa:ObjectA = {
    a:'a'
}
let Bb:ObjectB = {
    b:'b'
}
let AB = Object.assign(Aa,Bb)  // AB类型为 ObjectA & ObjectB 的交叉类型
console.log(AB);  // {a: 'a', b: 'b'}

类的混入

需要通过Object.getOwnPropertyNames获取当前遍历的原型对象,获取它原型对象上定义的所有属性

class ClassAa{
    public isA:boolean
    public funcA(){}
}
class ClassBb{
    public isB:boolean
    public funcB(){}
}
class ClassAB implements ClassAa,ClassBb{
    isA: boolean = false
    isB: boolean = false
    funcA:()=>void
    funcB:()=>void
    constructor(){}
}

function mixins(base:any,from:any[]){
    from.forEach(fromItem =>{
        Object.getOwnPropertyNames(fromItem.prototype).forEach(key=>{
            console.log(key);
            base.prototype[key] = fromItem.prototype[key]
        })
    })
}
mixins(ClassAB,[ClassAa,ClassBb])
const ab = new ClassAB()
console.log(ab);

创建classAB的实例,并输出,输出的funcA和funcB是有实际的函数体的

通过mixins函数,将这个ClassAa和ClassBb原型对象的属性和方法赋给ClassAB
因为ClassAa和ClassBb有函数funcA和funcB的类型定义,所以可以把funcA和funcB的函数实体直接赋给ClassAB,这个就是ts中的混入。

其他重要更新

async函数以及Promise

异步回调, 会放入回调队列, 所有同步执行完后才可能执行,所以下面代码会先打印2再打印1

setTimeout(()=>{
    console.log(1);
},1000)
console.log(2);  // 先打印2再打印1

使用Promise可以保证代码的执行顺序

使用Promise函数实现先打印1再打印2的功能

function getIndexPromise(){
    return new Promise<void>((resolve,reject)=>{
        setTimeout(()=>{
            console.log(1);
            resolve(); // 打印1之后再执行后面的操作
        },1000)
    })

}
getIndexPromise().then(()=>{
    console.log(2);
})

Async/await 实际上只是一种基于 promises 的糖衣语法糖

async function 用来定义一个返回 AsyncFunction 对象的异步函数。await 操作符用于等待一个 Promise 对象。它只能在异步函数 async function 中使用。

function getIndexPromise(bool){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log(1);
            if(bool){ resolve('a') }
            else { reject(Error('error')) }
        },1000)
    })

}
getIndexPromise(false).then((res)=>{
   console.log(res);
}).catch((error)=>{
    console.log(error);
})

async function asyncFunction(){
    const res = await getIndexPromise(true)
    console.log(res);
}
asyncFunction() // 在1秒之后打印出a

tsconfig.json支持注释

json文件里不允许有注释,但是tsconfig.json支持注释

动态导入表达式

使用ts提供的动态导入表达式可以实现按需加载

async function getTime(format:string) {
    const moment = await import('moment')
    return moment.default().format(format)
}
// 只有使用的时候才会动态导入moment模块
getTime('L').then(res=>{
    console.log(res);  // 03/30/2022

})

弱类型探测

任何只包含可选属性的类型都会被当作弱类型。当给类型为弱类型的值赋值时,如果这个值的属性与弱类型定义的没有任何重叠属性,就会报错

interface ObjIn{
    name?:string
    age?:number
}
let objIn = {
    sex:'man'
}
function printInfo(info:ObjIn){
    console.log(info);
}
printInfo(objIn)// 类型“{ sex: string; }”与类型“ObjIn”不具有相同的属性。

... 操作符

...操作符使用例子

function mergeOptions(op1:object,op2:object){
    return { ...op1,...op2 }  // ...obj 得到对象的所有属性
}
const mergeObj = mergeOptions({ a:'a'},{b:'b'})
console.log(mergeObj);  // {a: 'a', b: 'b'}

可以在泛型中使用...操作符

function mergeOptions<T,U extends string>(op1:T,op2:U){
    return { ...op1,op2 }  // ...obj 得到对象的所有属性
}
const mergeObj = mergeOptions({a:'a'},'name')
console.log(mergeObj);  // {a: 'a', op2: 'name'}

还可以从泛型变量中解析结果

function getExcludeProp<T extends { props:string }>(obj:T){
    const { props,...rest } = obj  // ...rest 接收除了props属性以外的剩余所有属性
    return rest
}
const objInfo = {
    props:'something',
    name:'zzz',
    age:18
}
console.log(getExcludeProp(objInfo)); // {name: 'zzz', age: 18}

声明文件(暂时看不懂)

使用ts进行开发的时候,不可避免的需要引用第三方的 JS 的库,但是默认情况下TS是不认识我们引入的这些JS库的。所以在使用这些JS库的时候, 我们就要通过声明文件告诉TS它是什么, 怎么用。

全局库

通过 <script> 标签引入第三方库,注入全局变量

在modules文件夹创建一个自定义库 handle.title.js

function setTitle(title){
    document && (document.title = title)
}

function getTitle(){
    return document?document.title:''
}

let documentTitle = getTitle()

使用命令npm install -D copy-webpack-plugin@5.0.2 安装webpack的插件,然后在build→webpack.config.js中配置如下代码

const CopyWebpackPlugin = require('copy-webpack-plugin')
const path = require('path')

module.exports = {
  ...
  plugins:[
   ...
    new CopyWebpackPlugin([
      {
        from:path.join(__dirname,'../src/modules/handle.title.js'),
        to:path.resolve(__dirname,'../dist')
      }
    ])
  ],
}



这个插件可以将handle.title.js打包到dist目录下,这样才能成功引用该库

在index.html文件中引用该库

<script src="./handle.title.js"></script>

使用npm run build打包,可以看到 handle.title.js 也在dist目录下。

引用成功后,handle.title.js中的 setTitle 、getTitle都是在全局中定义的了,documentTitle在全局环境中,应该在每一个js文件中都能用到,在一个ts文件中输出documentTitle,发生报错。

在ts文件中,使用外部的.js文件,需要先写.js文件的声明文件。

可以在tsconfig.json中配置文件的路径

  {
    "compilerOptions":{
     ...
    },
    //默认情况下,不写会把项目里所有的.d.ts文件和.ts文件都引进来
    "include": [
      "./src/**/*.ts",
      "./src/**/*.d.ts"
    ]
  }

global.d.ts

declare function setTitle(title:string|number):void
declare function getTitle():string
declare let documentTitle:string

模块化库

模块化库就是依赖模块化解析的库。一些库只能工作在模块加载器的环境下。 比如,像 express只能在Node.js里工作所以必须使用CommonJS的require函数加载。

UMD库

UMD模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)使用的模块。 许多流行的库,比如 Moment.js,就是这样的形式。

tsconfig.json配置详解

    /* Basic Options */
    "target": "es5",                          /* target用于指定编译之后的版本目标 version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "commonjs",                     /* 用来指定要使用的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": [
      "es6",
      "dom"
    ],                             /* lib用于指定要包含在编译中的库文件,这个我们在前面的课程中讲过一点,如果你要使用一些ES6的新语法,你需要引入ES6这个库,或者也可以写ES2015。 */
    // "allowJs": true,                       /* allowJs设置的值为true或false,用来指定是否允许编译JS文件,默认是false,即不编译JS文件。 */
    // "checkJs": true,                       /* checkJs的值为true或false,用来指定是否检查和报告JS文件中的错误,默认是false。 */
    // "jsx": "preserve",                     /* 指定jsx代码用于的开发环境: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* declaration的值为true或false,用来指定是否在编译的时候生成相应的".d.ts"声明文件。如果设为true,编译每个ts文件之后会生成一个js文件和一个声明文件。但是declaration和allowJs不能同时设为true。 */
    // "declarationMap": true,                /* 值为true或false,指定是否为声明文件.d.ts生成map文件 */
    // "sourceMap": true,                     /* sourceMap的值为true或false,用来指定编译时是否生成.map文件。 */
    // "outFile": "./",                       /* outFile用于指定将输出文件合并为一个文件,他的值为一个文件路径名,比如设置为"./dist/main.js",则输出的文件为一个main.js文件。但是要注意,只有设置module的值为amd和system模块时才支持这个配置。 */
    // "outDir": "./",                        /* outDir用来指定输出文件夹,值为一个文件夹路径字符串,输出的文件都将放置在这个文件夹。 */
    // "rootDir": "./",                       /* 用来指定编译文件的根目录,编译器会在根目录查找入口文件,如果编译器发现以rootDir的值作为根目录查找入口文件并不会把所有文件加载进去的话会报错,但是不会停止编译。 */
    // "composite": true,                     /* 是否编译构建引用项目 */
    // "removeComments": true,                /* removeComments值为true或false,用于指定是否将编译后的文件中的注释删掉,设为true的话即删掉注释,默认为false。 */
    // "noEmit": true,                        /* 不生成编译文件,这个一般很少用了。 */
    // "importHelpers": true,                 /* importHelpers的值为true或false,指定是否引入tslib里的辅助工具函数,默认为false。 */
    // "downlevelIteration": true,            /* 当target为“ES5”或“ES3”时,为“for-of”、“spread”和“destructuring”中的迭代器提供完全支持。 */
    // "isolatedModules": true,               /* isolatedModules的值为true或false,指定是否将每个文件作为单独的模块,默认为true,他不可以和declaration同时设定。 */

    /* Strict Type-Checking Options */
    // "strict": true,                           /* strict的值为true或false,用于指定是否启动所有类型检查,如果设为true则会同时开启下面这几个严格类型检查,默认为false。 */
    // "noImplicitAny": true,                 /* noImplicitAny的值为true或false,如果我们没有为一些值设置明确的类型,编译器会默认认为这个值为any类型,如果将noImplicitAny设为true,则如果没有设置明确的类型会报错,默认值为false。 */
    // "strictNullChecks": true,              /* strictNullChecks的值为true或false,这个配置项我们在前面课程讲过了,当设为true时,null和undefined值不能赋值给非这两种类型的值,别的类型的值也不能赋给他们, 除了any类型,还有个例外就是undefined可以赋值给void类型。 */
    // "strictFunctionTypes": true,           /* strictFunctionTypes的值为true或false,用来指定是否使用函数参数双向协变检查。还记得我们讲类型兼容性的时候讲过函数参数双向协变的这个例子: */
    // "strictBindCallApply": true,           /* strictBindCallApply的值为true或false,设为true后会对bind、call和apply绑定的方法的参数的检测是严格检测的 */
    // "strictPropertyInitialization": true,  /* strictPropertyInitialization的值为true或false,设为true后会检查类的非undefined属性是否已经在构造函数里初始化,如果要开启这项,需要同时开启strictNullChecks,默认为false。 */
    // "noImplicitThis": true,                /* 当 this表达式的值为 any类型的时候,生成一个错误。 */
    // "alwaysStrict": true,                  /* alwaysStrict的值为true或false,指定始终以严格模式检查每个模块,并且在编译之后的JS文件中加入"use strict"字符串,用来告诉浏览器该JS为严格模式。 */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* noUnusedLocals的值为true或false,用于检查是否有定义了但是没有使用的变量,对于这一点的检测,使用ESLint可以在你书写代码的时候做提示,你可以配合使用。他的默认值为false */
    // "noUnusedParameters": true,            /* noUnusedParameters的值为true或false,用于检查是否有在函数体中没有使用的参数,这个也可以配合ESLint来做检查,他默认是false。 */
    // "noImplicitReturns": true,             /* noImplicitReturns的值为true或false,用于检查函数是否有返回值,设为true后,如果函数没有返回值则会提示,默认为false。 */
    // "noFallthroughCasesInSwitch": true,    /* noFallthroughCasesInSwitch的值为true或false,用于检查switch中是否有case没有使用break跳出switch,默认为false。 */

/* Module Resolution Options */
    // "moduleResolution": "node",            /* moduleResolution用于选择模块解析策略,有"node"和"classic"两种类型,我们在讲模块解析的时候已经讲过了。 */
    // "baseUrl": ".",                       /* baseUrl用于设置解析非相对模块名称的基本目录,相对模块不会受baseUrl的影响。 */
    // "paths": {
    //   "*": ["node_modules/@types", "src/typings"]
    // },                           /* paths用于设置模块名到基于baseUrl的路径映射 */
    // "rootDirs": [
    //   "src/module",
    //   "src/core"
    // ],                        /* rootDirs可以指定一个路径列表,在构建时编译器会将这个路径列表中的路径中的内容都放到一个文件夹中. */
    // "typeRoots": [],                       /* typeRoots用来指定声明文件或文件夹的路径列表,如果指定了此项,则只有在这里列出的声明文件才会被加载。 */
    // "types": [],                           /* types用来指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载进来。 */
    // "allowSyntheticDefaultImports": true,  /* allowSyntheticDefaultImports的值为true或false,用来指定允许从没有默认导出的模块中默认导入。 */
    "esModuleInterop": true,                   /* 通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性 */
    // "preserveSymlinks": true,              /* 不把符号链接解析为其真实路径,具体可以了解下webpack和nodejs的symlink相关知识 */

    /* Source Map Options */
    // "sourceRoot": "",                      /* sourceRoot用于指定调试器应该找到TypeScript文件而不是源文件位置,这个值会被写进.map文件里。 */
    // "mapRoot": "",                         /* mapRoot用于指定调试器找到映射文件而非生成文件的位置,指定map文件的根路径,该选项会影响.map文件中的sources属性。 */
    // "inlineSourceMap": true,               /* inlineSourceMap值为true或false,指定是否将map文件的内容和js文件编译在一个同一个js文件中,如果设为true,则map的内容会以//# sourceMappingURL=然后接base64字符串的形式插入在js文件底部。 */
    // "inlineSources": true,                 /* inlineSources的值是true或false,用于指定是否进一步将.ts文件的内容也包含到输出文件中。 */

    /* Experimental Options */
    "experimentalDecorators": true,        /* experimentalDecorators的值是true或false,用于指定是否启用实验性的装饰器特性 */
    // "emitDecoratorMetadata": true,         /* emitDecoratorMetadata的值为true或false,用于指定是否为装饰器提供元数据支持,关于元数据,也是ES6的新标准,可以通过Reflect提供的静态方法获取元数据,如果需要使用Reflect的一些方法,需要引入ES2015.Reflect这个库 */
  // "files": [], // files可以配置一个数组列表,里面包含指定文件的相对或绝对路径,编译器在编译的时候只会编译包含在files中列出的文件。如果不指定,则取决于有没有设置include选项,如果没有include选项,则默认会编译根目录以及所有子目录中的文件。这里列出的路径必须是指定文件,而不是某个文件夹,而且不能使用* ? **/ 等通配符
  // "include": [], // include也可以指定要编译的路径列表,但是和files的区别在于,这里的路径可以是文件夹,也可以是文件,可以使用相对和绝对路径,而且可以使用通配符,比如"./src"即表示要编译src文件夹下的所有文件以及子文件夹的文件。
  // "exclude": [], // exclude表示要排除的、不编译的文件,他也可以指定一个列表,规则和include一样,可以是文件可以是文件夹,可以是相对路径或绝对路径,可以使用通配符。
  // "extends": "", // extends可以通过指定一个其他的tsconfig.json文件路径,来继承这个配置文件里的配置,继承来的文件的配置会覆盖当前文件定义的配置。TS在3.2版本开始,支持继承一个来自Node.js包的tsconfig.json配置文件.
  // "compileOnSave": true // compileOnSave的值是true或false,如果设为true,在我们编辑了项目中文件保存的时候,编辑器会根据tsconfig.json的配置重新生成文件,不过这个要编辑器支持。
  // "references": [] // 一个对象数组,指定要引用的项目
posted @ 2022-04-17 23:33  小风车吱呀转  阅读(86)  评论(0编辑  收藏  举报