四. TypeScript

273 阅读9分钟

1.基础

1.1 介绍

  • TypeScript是一种由微软开发的开源,跨平台的编程语言, 最终会编译为JavaScript代码.
  • 2012年10月微软发布了首个公开版的TypeScript, 2013年6月19日,在经历了预览版之后微软正式发布了正式版的TypeScript.
  • TepyScript的作者是安德斯.海尔斯伯格,C#的首席架构师.它是开源和跨平台的编程语言.
  • TepyScript扩展了JavaScript的语法,所以任何现有的JavaScript程序都可以运行在TepyScript环境中.
  • TepyScript是为大型应用的开发而设计, 并且可以编译为JavaScript.
  • TepyScript是JavaScript的一个超集.主要是提供了类型系统和对Es6+ 的支持,它由Microsoft,代码开源于GitHub上.

1.2 TepyScript的特点

  • 始于JavaScript,归于JavaScript
  • 强大的类型系统
  • 先进的JavaScript

1.3 安装

  • npm install -g typescript
  • tsc -V

1.4 ts代码编译

(1) html中直接引入了ts的文件,浏览器会报错.  但谷歌浏览器可以运行
    如果.ts文件中只有单纯的js代码所有浏览器都可以运行.
(2) 如果ts文件中有ts代码, 那么需要把这个ts文件编译成js文件, 手动编译:  tsc  xxx.ts
(3) ts 中形参有类型修饰, 编译成js后无类型修饰.  let会被编译成var

(4) vscode自动编译: tsc --init 来生成 tsconfig.json文件
    outDir: 编译后,js文件的输出目录
    strict: false   取消严格模式
    
    启动:  终端--> 运行任务-->所有任务--> 监视
        vscode将会自动的 把ts文件 编译成js文件
        
    tsc --watch 自动生成配置文件
    tsc --noEmitOnError  xxx.ts  发出错误
    
(5)
tsc是TypeScript  compiler
(6)
测试ts代码有两种方案:  第一个,通过webpack搭建一个ts环境
第二个, 安装ts-node,  使用ts-node  aaa.ts,  之后编译再跑 node.
npm install ts-node -g
npm install tslib @types/node -g


1.5 ts新特性

  • 类型的注解: 一种轻量级的为函数或变量添加的约束
  • 接口: interface
  • 通过webpack打包ts
npm init -y  产生package.json
tsc --init  产生tsconfig.json

yarn add -D typescript
yarn add -D webpack webpack-cli
yarn add -D webpack-dev-server
yarn add -D html-webpack-plugin clean-webpack-plugin
yarn add -D ts-loader
yarn add -D cross-env

2.语法

  • let/const 变量: 数据类型 = 值

2.1 基础类型

javaScript 基本类型:  number, string, boolean, null, sambal, BigInt, undefined
JavaScript 引用类型:  function, object, Date, ......

ts中变量一开始是什么类型,后期赋值时,只能用当前类型的数据进行赋值,是不允许用其他类型的数据赋值给当前这个变量的.

    (1) 布尔类型: boolean,   let flag: boolean =true
    
    (2) 数字类型: number,  
        * 十进制: 10
        * 二进制: 0b1010
        * 八进制: 0o12 
        * 十六进制: 0xa
        
    (3) 字符串类型: string
    
    (4) nullundefined类型: 值与变量类型不同不能赋值, 但是undefined  null, 可以把这两个赋值给其他类型的变量.
    let abc = null 如果不指定abc的类型,那么abc将会被推导出来为any类型.
    let abc:string = null
    let abc:string = null 如果开启严格模式,这行将会报错
    let aa: null = null 如果指定为null类型,那么只能赋值为null
    
    (5) 
    数组定义: let arr1: number[] = [10,20,30,40] 这种写法是推荐的.
    数组定义: let arr2: Array<number> = [100,200,300]  这种写法是不推荐的.
    (6) object类型:      
     cosnt info = {
         name: "Tom",
         age: 18
     }
     这个info变量的类型默认是会被推导出来的, 这样info.name 是可以取出来值的.
     
     可以指定为object类型
     const info:object = {
         name: "Tom",
         age: 18
     }
    但是取值, info.name 是取不出来值的, 虽然指定的是object类型,但是不代表有name这个属性.
    
    对象类型: 
    function test(point: {x:number,y:number, z?:number}) {}
    
注: ----------以上是JavaScript里边有的类型, 以下是TS特有的类型----------
    
    (7) any类型: 
    let aa: any = 234   any类型的变量可接收任意类型的数据,也可以赋值给任意类型的变量
    let arr: any[] = [111,'weefg',null]
    
    let name   不指定类型将会被推断为any类型
    函数的参数不指定类型也会被推断为any类型
    在开发中,通常情况下可以不写返回值的类型,不写的话他会自动推导的,他会把我们return 后边这里的返回值类型,来作为我们函数的返回值类型的.
    函数对参数的类型有限制,对参数的个数也是有限制的
    
    (8) unknown类型: 它表示不确定的类型
    unknown类型变量只能赋值给anyunknown类型
    
    (9) void类型: 函数类型没有返回值类型时,用void.
    function showMag():void {
        console.log("hello,nasdfiooasf,tom,jack")
    }
    当一个函数没有返回值的时候, 默认返回undefined的.
    这里返回undefinednull, 这个void都是可以接收的.
    
    let dd: void = undefined, 定义一个void的变量,可以接收一个undefined的值,但是意义不是很大.
    
    (10) never类型: 表示永远不会返回值的类型, 比如一个函数:
    如果一个函数中是一个死循环或者抛出一个异常,name这个函数会返回东西吗?
        不会,那么写void类型或者其他类型作为返回值类型都不合适,我们就可以使用never类型.
    
    (11) 元组类型定义后,里面的数据必须和定义数组的时候类型是一致的 ---> (类型和个数一致)
    * 元组类型: let arr3: [string, number, boolean] = ['tom', 100, true]

    (12) 联合类型: number|string
       
    (13) 字面量类型
    "hello" 这个字符串也是可以作为一个类型的,叫做字面量类型
    const msg:"hello" = "hello"
    const num:123 = 123
    
    字面量类型的意义,就是结合 联合类型
    let align: 'left'|'right'|'center' = 'left'
    
    (14) 字面量推导: 没指定类型时, 会将这个值的类型赋值给这个变量.
    let ttt = 100
    ttt = 'jack'   // 这行会报错, 因为ttt被推断出 number类型
    * let aaa
    * console.log(aaa)   aaa是any类型
    * aaa= 999
    * aaa = 'tom'   // 这行不会报错,, 因为aaa 被推断是any类型
    
    (15) 类型断言: 
    有时候TypeScript 无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions). 比如我们通过document.getElementById, TypeScript只知道该函数会返回HTMLElement,但并不知道它具体的类型.
    
    在一个宽泛的类中断言他是某个小类
    
    if( (<string>msg).length ) {}
    或者
    if( (msg as string).length ) {}
     
     如果断定msg一定是有值的一定是一个非空的不是null也不是undefined
     if(msg!.length) {}  叹号保证msg一定有值, 这个就是非空类型断言
     
     !!操作符: 将一个其他类型转换成boolean类型,类似于Boolean(变量)的方式. 类似于
     
    (16) 类型别名
        type yyy = {
        x:number,
        y:number,
        z?: number,
    }
    type ttt = string | number | boolean
    function test(id: ttt) {}
    
    (17) 可推导的this类型???
    
    (18) 枚举类型:
    枚举类型是为数不多的TypeScript特性之一 
    枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型;
    枚举允许开发者定义一组命名常量,常量可以是数字,字符串类型;
    
    
    枚举类型: enum Color{
        RED=10,
        GREEN,
        BLUE
    }
    let color: Color = Color.RED
    通过下标访问枚举 console.log(Color[3])
    里面的每个数据值都可以叫元素,每个元素都有自己的编号,编号是从0开始的,一次的递增加1
   

2.2 类型缩小

(1)什么是类型缩小呢?
    类型缩小的英文是Type Narrowing
    我们可以通过类似于typeof padding === "number"的判断语句,来改变TypeScript的执行路径
    在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为缩小
    而我们编写的typeof padding === "number"可以称之为类型保护(type guars)
(2)常见的类型缩小(类型保护)有如下几种:
    typeof
    平等缩小
    instanceof
    in
    等等

(3) 具体说明
typeof的使用:
    type IDType = number | string
    function printID(id: IDType) {
        console.log(id)   // 在这里一定是一个联合类型
        if(typeof id === 'stirng') {
            console.log(id.toUpperCase())   // 在这里一定是一个字符串类型
        }
        else {
            console.log(id)   // 在这里一定是一个 数字类型
        }
    }
平等类型缩小 ==, !=, switch:
    type Dir = 'left'|'right'|'top'|'bottom' 
    function printDir(d: Dir) {
        if(d == 'left') { console.log(d) }
    }
instanceof, 某一种实例是不是某一种类型:
    function printTime(time: string|Date|Reg) {
        if(time instanceof Date) {   // 判断这个实例是否属于 某个类创建出来的实例
            console.log(time.toUTCString)
        }
        else {  }
    }
in :
    type Fish = {
        name: string,
        age: number,
        swimming: () => void
    }
    type Dog = {
        running: () => void
    }
    function walk (animal: Fish | Dog) {
        if("swimming" in animal) {   // 判断swimming这个key是否在animal这个对象里边
            animal.swimming()
        }
        else {
            animal.running()
        }
    }
    
    const fish:Fish = {
        swimming() {
            console.log("swimming")
        }
    }
    walk(fish)
    

2.3 类

2.3.1 基础

(1) 定义
class Person {
    // 定义属性
    name:string,
    age: number,
    gender: string,
    
    // 定义构造函数: 为实例化对象的时候,可以直接对属性的值进行初始化
    constructor(name: string='tom', age: number=16, gender: string='女') {
        this.name = name
        thia.age = age
        thia.gender = gender
    }
    
    // 定义实例方法
    sayHi(str: string) {
        console.log(`我是${this.name}, 今年已经${this.age}岁了, 是个${this.gender}生`, str)
    }
}


(2) 继承
* 子类,派生类  父类,基类,超类

class Student extends Person {
    code: string
    constructor(name: string, age: number, gender: string, code: string) {
        super(name, age, gender)   // 调用父类的构造函数
        this.code = code   // 学号
    }
    sayHi() {
        super.sayHi('呵呵呵')
        console.log('我是学生类中的sayHi方法')
    }
}


(3) 多态: 父子各个类中,针对相同的方法,会产生不同的行为
    父类的类型创建子类的对象 const aa:Animal = new Dog('小黄')
    父类型的引用 指向了 子类型的对象
    
    function showRun(ttt: Animal) {
        ttt.run()
    }
    
    showRun(dog1)
    showRun(pig1)

2.3.2 类的成员修饰符

(1) 修饰符, 按照访问范围划分
    public: 修饰的是在任何地方可见,公有的属性或方法. 不写时默认是public.
    private: 修饰的是仅在同一类中可见, 私有属性或私有方法.  
             不想被外部实例对象调用, 也不想被子类继承的 属性或方法.
    protected 修饰的是仅在类和其子类中可见,受保护的属性或方法.
              不想被外部实例对象调用, 子类可以继承的 属性或方法
    
    readonly : 只读的,不能被修改, 类中的普通方法也不能修改这个值, 实例对象点 也不能修改这个值.
               构造函数可以修改这个值
               如果这个值是对象类型的,那么可以更改里边的某个属性
    
    构造函数中的name参数, 一旦使用readonly进行修饰后, 那么该name参数可以叫参数属性
    构造函数中的name参数, 一旦使用readonly进行修饰后, 那么Person类中就有了一个name的属性成员
    构造函数中的name参数, 一旦使用readonly进行修饰后, 外部也是无法修改类中的name属性的
    
    构造函数中的name参数, 一旦使用public进行修饰后, 那么Person类中 也有了一个name的属性成员---这个可以被修改
    构造函数中的name参数, 一旦使用private进行修饰后, Person类中就有了一个私有的name属性了
    构造函数中的name参数, 一旦使用protected进行修饰后, Person类中就有了一个 受保护的name属性了
    
    
    
(2) 修饰符, 按照归属划分
    * 实例方法 或 实例属性 : 定义在构造函数中, 实例化时可以通过传参来赋值
    * 类方法 或 类属性: 定义在类中, 属于类的, 类名点调用 也可以 对象点调用.  只要更改各个实例化出来的对象访问该 属性或方法 都会被更改.
    * 静态方法 或 静态属性 static: 既不访问类属性和类方法, 也不访问实例属性和实例方法. 用类名点调用

(3) 存取器: get()  set()
class Person {
    fname: string,
    lname: string,
    
    constructor(fname: string, lname: string) {
        this.fname = fname
        this.lname = lname
    }
    
    
    get fullname() {
        return this.fname + '_' + this.lname
    }
    set fullname(value) {
        let arr = value.split('_')
        this.fname = fname
        this.fname = lname
    }
}

2.3.3 抽象类

  • 包含抽象方法, 也可以包含实例方法, 抽象类不能被实例化
  • 接口里只能有抽象方法, 不能有实例方法, 接口也不能被实例化.
abstract class Animal {   // 抽象不能被实例化 
        abstract eat()   // 抽象方法必须在抽象类中,   子类中必须实现该抽象方法或属性
}

2.4 接口

  • 通过类型(type)别名来声明对象类型
  • type MyType = {name: string, age: number}
  • 另外一种方式声明对象类型: 接口interface
  • 接口是对象的属性和方法(行为) 的抽象. 接口是一种类型,是一种规范, 是一种规则, 是一个能力,是一种约束
1.对象中使用 接口指定类型
interface Iperson {
    readonly id: number,   // 只读取的属性
    name: string,
    age: number,
    sex?: string,   // 该属性, 可有可无
}

// 定义一个对象 使用这个接口来约束类型
const person: IPerson = {
    id: 1,
    name: 'tom',
    age: 11,
    sex: '女'
}
2. 索引类型 中 使用接口
interface Taa {
    [index: number] :string
}
const frontLanguage: Taa = {
    0: "HTML",
    1: "CSS",
    2: "JavaScript",
    3: "Vue",
    "abc": "cba" // 这行会报错
}

3.函数的类型 (可选参数放在最后。 默认值参数可以随便放,最好也放在后边。 剩余参数。 函数重载)
                函数重载: 定义同名的函数, 函数名相同,参数个数或类型不同。
                function add(num1: number, num2: number): number;   // 重载的函数
                function add(num1: string, num2: string): string;   // 重载的函数
                function add(num1:any, num2: any): any {   // 有函数体的 实现函数
                    return num1 + num2
                }
                cosnt result = add(20, 11)   // 在使用的时候,必须匹配到上边的重载函数, 
                    如果没有匹配到我们的重载函数,也是不能去调用到我们的实现的函数的.
                    在函数重载的过程当中,实现函数是不能直接被调用的.  这个是TypeScript语法规定好的.
                
    (1) 在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数 也可以作为返回值进行传递).
function foo(n: number) { console.log(n) }
function bar(myfoo: (num:number) => void) {
    myfoo(33)
}
bar(foo)

    (2) 函数中 使用接口来约束类型
interface ISearchFunc {
    // 定义一个签名
    (source:string, subString:string):boolean
}

//定义一个函数,使用该接口
const searchString: ISearchFunc = function (source:string, subString:string):boolean {
    return source.search(subString) > -1
}


4. 类类型
(1)
class Person {
    name: stirng = "tom"
    
    sing() {}
}

function(p: Person) {}

(2) 类中使用接口来指定类型
interface IFly {
    fly()
}
interface ISwim {
    swim()
}

class Person implements IFly,ISwim {
    fly() {
        console.log('我会飞了, 我是超人')
    }
    swim() {
        console.log('游泳')
    }
}
总结: 类可以通过接口的方式,来定义当前这个类的类型
      类可以实现一个接口, 类也可以实现多个接口, 要实现接口中的方法


5. 接口可以继承其他的多个接口
interface IFly {
    fly()
}
interface ISwim {
    swim()
}
interface IMyFlyAndSwim extends IFly, ISwim {

}

class Person3 implements IMyFlyAndSwim{
    fly() {
        console.log('我飞了')
    }
    swim() {
        console.log('游泳')
    }
}

总结: 接口和接口之间叫继承extend
     接口和类之间叫实现implements
     类和类之间叫继承extend
     
6. 交叉类型
交叉类型也可以把 两个类型给结合在一起

type wtype = number & string

interface Iaaa {
    swimming: () => void
}
interface Ibbb {
    flying: () => void
}

type Myccc2 = Iaaa | Ibbb
type Myccc = Iaaa & Ibbb
const obj: Myccc = {
    swimming() {},
    flying() {}
}

7. interface 和 type的区别
我们发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
(1)
如果是定义非对象类型,通常推荐使用type, 比如Direction,Alignment,一些function; 联合类型尽量 也用 type来定义.
(2)
如果是定义对象类型,那么他们是有区别的:
interface可以重复的对某个接口来定义属性和方法
而type定义的是别名,别名是不能重复的

interface IFoo {
    name: string
}
interface IFoo {
    age: number
}
定义接口的时候如果你名称相同, 它相当于会把我们所有属性做一个合并的
const foo: IFoo = {
    name: "why",
    age: 18
}

8. 字面量赋值 (擦除操作)
interface IPerson {
    name:  string
    age: number
    height: number
}

const p: IPerson {
    name: "tom",
    age: 18,
    height:  1.99,
    address: "广州市"    // 这行会报错的
}


一下这种方式就不会报错
const info = {
    name: "tom",
    age: 18,
    height:  1.99,
    address: "广州市"
}
const p:IPerson = info   // freshness擦除,它会在类型检测的时候 (address:"广州市") 会把这行擦除掉.


2.5 泛型 (类型的参数化)

2.5.1 泛型函数

function sum<Type33>(arg: Type33, p: Type33): Type33 { return arg }
把用到Type33的地方提取到前边,进行一个参数化而已.
调用者告诉我Type是一个什么类型
sum<number>(20, 30)  可以简写成sum(20, 30)

编译器会自动的根据我们传入的 值的参数来自动的进行 "参数推断"

Array<Type> 表示Type类型的数组
Type[] 表示Type类型的数组

2.5.2 泛型接口

interface GenericIdentity<Type> {
    (arg: Type): Type;
}

2.5.3 泛型类

class GNumber<Type> {
    value: Type
    add: (x:Type, y: Type) => Type
}

2.5.4 泛型约束

(1)
interface Leng {
    length: number
}
function test<Type extends Leng>(arg: Type):Type {
    return arg
}

(2)泛型约束中使用类型参数
<Key extends keyof Type>

function getProperty<Type, Key extends keyof Type>(obj:Type, key: Key) {
    return obj[key]
}

let x = {
    a: 1,
    b: 2,
    c: 3,
    d: 4,
}

getProperty(x, 'a')

2.5.5 泛型中使用类类型

function test<Type>( c: {new ():Type} ):Type {
    return new c()
}

2.5.6 keyof类型操作符

type Point = { x: number;  y:number }
type P = keyof Point
const p1:P = 'x'
const p2:P = 'y'

2.5.7 typeof类型操作符

let s = "hello"
let n: typeof s
n = 'world'
根据值 取他的类型

2.5.8 索引访问类型

(1)
type Person = {
    age: number;
    name: string;
    alive: boolean
}

type age = Person["age" | "name"]

type aaa = Person[keyof Person]

type p = 'alive' | 'name'
type bbb = Person[p]

(2)
const MyTest = [
    {name: 'tom', age: 11},
    {name: 'kobe', age: 7},
    {name: 'jack', age: 9},
]

type mm = typeof MyTest[number]
const t:mm = {name: 'Jams', age: 13}

type age = typeof[number]['age']
const a:age = 13

2.5.9 条件类型

(1)
interface Animal {
    live(): void
}

interface Dog extends Animal {
    woof(): void
}

type aaa = Dog extends Animal ? number:string

(2)

type NameOrId<T extends number | string> = T extends number? IdLabel : NameLabel

function createLabel<T extend number | string> (idOrName: T): NameOrId<T> {

}

(3) 条件类型约束
type MessageOf<T> = T["message"]  错误

type MessageOf<T extends {message: unknown}> = T["message"]  正确
interface Email {
    message: string
}
interface Dog {
    bark():void
}

type EmailMessageContents = MessageOf<Email>
const emc: EmailMessageContents = "balabala..."

type DogMessageContents = MessageOf<Dog>
const dmc: DogMessageContents = 'error' as never


(4) 条件类型内推理???
(5) 分布式条件类型???

3.其他

  • 模块化, ts命名空间
  • 声明:
类型的查找: 之前我们所有的TypeScript中的类型,几乎都是我们自己编写的,但是我们也有用到一些其他的类型.

const imageEl = document.getElementById("image") as HTMLImageElement
大家是否会奇怪,我们的HTMLImageElement类型来自哪里呢?甚至是document为什么可以有getElementById的方法呢?

其实这里就涉及到TypeScript 对类型的管理和查找规则了:

我们这里先给大家介绍另外一种TypeScript文件: .d.ts文件
    我们之前编写的TypeScript文件 都是.ts文件, 这些文件最终会输出.js文件, 也是我们通常编写代码的地方
    还有另外一种文件.d.ts文件 它是用来做类型的声明(declare). 
    它仅仅用来做类型检测,告知typescript我们有哪些类型.
(1) 内置类型声明: 
    是TypeScript自带的,帮助我们内置了JavaScript运行时的一些标准化API的声明文件
    包括比如Math,Date等内置类型,也包括DOM API,比如Window Document等
(2) 外部定义类型声明:(第三方库)
    方式一: 在自己库中进行类型声明(编写.d.ts文件), 比如axios
    方式二:通过社区的一个公有库DefinitelyTyped存放类型声明文件
        该库的github地址: https//...
        该库查找声明安装方式的地址: https://www.typescriptlang.org/dt/search?search=
        比如我们安装react的类型声明: npm i @types/react -D
(3) 自定义类型声明:
    自己建.d.ts文件
        declare module 'lodash' {   // 声明一个模块
            export function join(arr: any[]):void {}
        }
        declare 可以声明 模块/函数/类/
声明文件: 
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.png'
declare module '*.svg'
declare module '*.gif'
declare module '*.vue' {
    import { DefineComponent } from 'vue'
    const ...
}

声明命名空间:
declare namespace $ {
    export function ajax(setting: any):any
}

  • 装饰器
就是一个方法,可以注入到类, 方法, 属性参数上来.  扩展类,属性,方法,参数的功能.

(1) 类装饰器 (无法传递参数)
function fun1(target: any) {   // 这个target装饰谁, 传递过来的就是谁, 装饰类那么传递过来的就是类,装饰方法target接收到的就是一个方法.
    target.prototype.userName = "张三" 
}

@fun1   // 这样写 等同于把Person1当参数传给了fun1,  fun1(Person1)
class Person1 {
    
}

let p1 = new Person1 {}
p1.userName会打印张三

(2) 装饰器工厂

function fun2(options: any) {
    return function(target: any) {
        target.prototype.userName = options.name
        target.prototype.userAge = options.age
    }
}

@fun2({
    name: '张三',
    age:18
})
class Person2 {}

let p2 = new Person2()
console.log(p2)


(3) 装饰器组合 (就是多个装饰器)
如果有多个装饰器装饰某个类的时候,会先从上至下执行所有的装饰器工厂, 
拿到所有真正的装饰器, 然后再从下至上的执行所有的装饰器:

(4) 属性装饰器
function fun4(target: any, attr: any) {   // target代表这个类, attr代表这个
    
}

class Person4 {
    @fun4
    userName: string
}

(5) 方法装饰器
function fun5(target: any,propertyKey: string, descriptor: PropertyDescriptor) {   // target代表这个类, attr代表这个
    
}

class Person5 {
    @fun5
    sing() {
        console.log('singing')
    }
}



注: 用之前要在config.ts里边先配置
"compilerOptions" : {
    "experimentalDecorators": true
}