前端开发日记 # Typescript

357 阅读11分钟

简介

JS 的升级版,借鉴静态强类型语言的特性,拥有类型检测、语法扩展、工具属性等,更适合大型企业级项目开发。

啥是强类型语言?
变量定义后,就不允许改变该变量数据类型

啥是静态类型语言?
在编译阶段就确定所有变量的数据类型
啥是动态类型语言?
在运行阶段才确定所有变量的数据类型

为何要使用

javascript是一门动态的、弱类型的编程语言,诞生时,只是为了解决网页一些简单的特效,而非当前复杂的大型企业应用,所以使用javascript开发存在以下问题

  1. 错误无法提早暴露,只有运行时才会暴露,因此需要测试的成本比强类型语言更高
  2. 开发工具无法有很好的智能提示,因此要求编码功底更高
  3. 重构不靠谱
  4. 给代码增加许多不必要的类型判断

typescript针对以上问题都进行了解决,还具有以下优势:

  1. 编译期进行类型检测,规避大量低级错误
  2. 引入泛型
  3. 强大的d.ts声明文件
  4. 兼顾灵活性

安装

全局安装

npm i typescript -g

一般只是学习时才会这样安装,真正开发项目时不会这样安装

局部安装

npm i typescript -D

开发项目时配合前端构建工具(Webpack)一起使用,更为常用

配置

tsconfig.jsonTS编译器 的配置文件,TS编译器 根据 它的配置项 进行编译

如何快速生成tsconfig.json配置文件
tsc --init

include

指定哪些.ts文件需要编译,这个配置项优先级高于compilerOptions.rootDir项

{
    "include": "./src/**/*"
}

路径: 
** 代表任意目录(有或没)
 * 代表任意文件

exclude

指定哪些.ts文件不需要编译

注意:该配置项有默认值,默认node_modules,bower_components,jspm_packages 和 <outDir>目录

{
    "include": [
        "./src/**/*"
    ],
    "exclude": [
        "./src/home/**/*"
    ]
}

路径: 
** 代表任意目录
 * 代表任意文件

compilerOptions

  1. target 指定编译成ES的版本,默认ES3

虽然TS编译器会根据target指定的ES版本进行编译,但这种仅限于转译语法,例如:箭头函数转普通函数,let/const声明转var声明等,但对于ES6新增的api无法转译,例如:Promise、Array.prototype.include()无法转译,因此,我们还需要使用babel对代码进行编译处理,才能够兼容IE8/9/10等旧版本浏览器

结论:TS编译器与Babel在语法转译上功能重复,但TS编辑器无法对ES6新的API进行转译,为了兼容老版本浏览器,只能使用Babel,就是说TS编译器无法完全替代Babel

  1. module 指定编译成哪种模块化规范,一般指定为commonjs
  2. outDir 指定编译后文件输出的目录
  3. lib 指定项目需要使用的库(环境),默认是ES5
  4. allowJs 是否允许对JS文件进行编译,

有时候项目使用到的第三方库是JS写的,而并非是TS写的,所以需要对JS进行编译输出

  1. checkJs 是否检查JS代码是否符合代码规范,

与 allowJs 配合使用,但如果是第三方代码,一般无须开启检查

  1. removeComments 是否移除注释
  2. onEmitOnError 是否在编译出错时不进行输出
  3. alwaysStrict 编译后的文件是否使用严格模式

代码使用了模块化规范后,此配置项无效

  1. noImplicitAny 不允许隐式的Any类型数据
  2. noImplicitThis 不允许不明确类型的This
{
    "compilerOptions": {
        "target": "es6",
        "module": "es6",
        "outDir": "./dist",
        "allowJs": true,
        "checkJs": true
        
    }
}
  1. declaration 是否在编译TS文件后生成相应.d.ts声明文件,一般开启,需要导出才会生成内容
  2. rootDir 指定源码的根目录,TS编译器就会编译此目录下及其子目录下的TS文件
  3. esModuleInterop 设置true表示允许依赖库中出现 export = 这种兼容规范导出格式,TS文件可以使用 import from 导入

类型

类型声明

  • 使用场景
  1. 变量
  2. 函数参数
  3. 函数返回值

通过类型声明,可以指定 变量函数入参函数返回值 的类型,使其仅可以存储某类型的值

  • 语法
let 变量名: 类型;

let 变量名: 类型 = 值;

function fn(参数名: 类型): 类型{}

类型推断

发生场景:变量声明

当变量的声明和赋值同时进行时,可以省略类型声明,TS编译器会根据赋值的类型给变量声明类型

const title = '123' // string
const num = 123  // number

any与unknown区别

1、any是任何类型的父类,也是任何类型的子类,不安全(能不用就不用) 2、unknown是任何类型的父类,但不是任何类型的子类,安全

数据类型

类型说明例子
number任意数字let a: number;
string任意字符串let a: string;
boolean布尔值let a: boolean;
字面量本身let a: 10;
any任意类型let s: any;
unknown任意类型let s: unknown;
void无(undefined、null)function test(): void{}
never抛出错误function test(): never{}
object对象let a: object;
array数组let a: string[]; 或 let t: Array<number>;
tuple固定长度数组let a: [string, number];
enum枚举
// 枚举
enum Gender{
    Male: 0,
    Female: 1
}

let i: {name: string, sex: Gender};
i = {
    name: 'wt',
    sex: Gender.Male
}

类型别名

type mt = 1 | 2 | 3 | 4;

let d: mt;

联合类型

可以给变量声明多个类型,例如:

let num: number | string;
num = 10;
num = 'hello'

变量要同时满足,使用很少,例如:

let obj: {name: string} & {age: number};
obj = {
    name: 'wt',
    age: 18
};

面向对象

由于Javascript是一门基于对象的编程语言,而非纯面向对象,所以在ES6之前,使用面向对象思想编写代码是比较复杂而且有点不好理解的,但在ES6出现之后,使用面向对象编程就变得简单多了,现在TS出现之后,让面向对象编程更加完整(新增了接口,抽象类等),更加严谨(借鉴JAVA)

构造函数

当对象创建时,都会自动调用构造函数,有且只调一次,而且构造函数内this指向当前创建的对象实例

作用:初始化对象的属性(数据)

语法如下:

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

const p = new Person('小明', 18)

封装

为了让数据(属性)和 功能(方法)更安全,我们必须使用封装,这是面向对象的三大特征之一

  • public 公有属性,可以任意位置修改与访问,没写默认就是公有,可继承
  • protected 保护属性,只能在类内部进行修改与访问,可继承
  • private 私有属性,只能在类内部进行修改与访问,不可继承
class Person{

    name: string; // 默认是public
    public age: number; 
    protected sex: string; 
    private hobby: Array<string>;
    
    getHobby(){
        return this.hobby
    }
    
    setHobby(val: Array<string>){
        this.hobby = val
    }
    
    // TS提供改进版,使用时,就可以直接‘对象.属性名’
    get name(){
        return this.name
    }
    
    set name(val: string){
        this.name = val
    }
}

继承

也是面向对象的三大特征之一,用于提高代码的复用性和可扩展性

语法

使用关键字extends

class Animal{
    name: string;
    age: number;
    
    sayHello(){
        console.log("动物叫")
    }
}

class Dog extends Animal{
    sex: string;
    
    // 重写
    sayHello(){
        console.log("汪汪")
    }
}
如何扩展属性或方法

扩展属性,必须要用到super关键字,而扩展方法就不一定(可用可不用)

super代表父类对象

class Animal{
    name: string;
    age: number;
    
    constructor(name: string, age: number){
        this.name = name;
        this.age = number;
    }
    
    sayHello(){
        console.log("动物叫")
    }
}

class Dog extends Animal{
    sex: string;
    
    constructor(name: string, age: number, sex: string){
        super(name, age)
        this.sex = sex;
    }
    
    // 重写方法时可以继承父类原本方法
    sayHello(){
        super.sayHello();
        console.log('啦啦啦')
    }
}

多态

也是面向对象的三大特征之一,利用方法重写原理,可配合 抽象类接口 实现

class Animal{
    say(): void{
        console.log('叫')
    }
}

class Dog extends Animal{
    say(): void{
        console.log("汪")
    }
}

class Cat extends Animal{
    say(): void{
        console.log("喵")
    }
}

抽象类

如果同时符合以下几点,就可以使用抽象类

1、定义专门作为父类使用的类
2、该类不能用来实例化
3、该类要对子类进行方法约束(抽象方法),但同时也可以提供公用方法实现

注意:抽象类定义时,需要用到abstract关键字

abstract class Animal{
    name: string;
    age: number;
    
     constructor(name: string, age: number){
        this.name = name;
        this.age = number;
    }
    
    // 抽象方法
    abstract sayHello(msg: string): void;
}

class Dog extends Animal{
    
    // 实现抽象方法
    sayHello(msg: string){
        console.log("汪汪")
    }
}

接口

接口用于制订规范,对目标进行约束,接口总共有4种

对象属性接口

对对象属性进行约束,与type定义一样

interface Person{
    firstName: string;
    lastName: string;
}

function fn(name: Person): void{
    console.log(`姓:${name.firstName},名:${name.lastName}`)
}

// 调用一,直接传对象,只能有2个属性
fn({
    firstName: '李'lastName: '四'
})

// 调用二,传对象的引用,除了包含2个属性,还可以有其他属性
const obj = {
    firstName: '李'lastName: '四',
    fullName: '搜索'
}

fn(obj)

带有可选项属性的接口

interface Person{
    firstName: string;
    lastName: string;
    age?: number;
}
函数接口

对函数进行约束,一般用于函数参数是函数时,指定函数结构的自定义类型

interface FnSum{
    (a: number, b: number): number;
}

const sum: FnSum = function(x: number, y: number): number{
    return x+y
}

console.log(sum(1, 4))
数组接口 (不常用)

对数组进行约束

interface ArrItf{
    [index: number]: string;
}

const temp: ArrItf = ['zhangsan', 'sdfff', 'abc']
类接口

对类进行约束,非常常用

interface Animal{
    name: string;
    sayHello(msg: string): void;
}

class Dog implements Animal{
    name: string;
    
    constructor(name: string){
        this.name = name
    }
    
    sayHello(msg: string): void {
        console.log("xxx")
    }
}

如果还需要继承的话,按下面例子

interface Animal{
    name: string;
    sayHello(msg: string): void;
}

class Test{
    run(){
        console.log("xxx")
    }
}

class Dog extends Test implements Animal{
    name: string;
    
    constructor(name: string){
        this.name = name
    }
    
    sayHello(msg: string): void {
        console.log("xxx")
    }
}
接口与抽象类区别

1、接口使用关键字是implements,而抽象类使用关键字是abstractextends
2、抽象类可以有抽象方法和普通方法,而接口只能有抽象方法(类似)

接口的继承

接口与类相似,也可以继承

interface AnimalImpl{
    eat(): void;
}

interface DogImpl extends AnimalImpl{
    say(): void;
}

class Dog implements DogImpl{
    eat(): void{
        alert('吃');
    }
    
     say(): void{
        alert('说');
    }
}

泛型

是一种JS不具备,而TS才具备的语法特性,与 函数重载 功能相似,但实际应用上,两者并不同

应用场景

在定义 函数接口 时,如果遇到类型无法确定,而要到调用时才能确定的,就可以使用泛型

语法

// 单个泛型
function fn<T>(a: T): T{
    return a;
}

// 多个泛型
function test<T, K>(a: T, b: K): T{
    return a;
}

// 限定泛型的范围,T必须是Person的实现类(子类)
interface Person{
    name: string;
}

function go<T extends Person>(a: T): string{
    return a.name;
}

// 使用了TS编译器的自动类型推断
fn(10);
test(10, 'abc');

// 有时遇到复杂的类型时,TS编译器无法自动推断出来,此时需要手动指定(推荐)
fn<string>('wt')
test<number, string>(10, 'wt')

泛型函数

function test<T>(a: T): T{
    return a;
}

泛型类

class Dog<T>{
    name: T;
    
    constructor(name: T){
        this.name = name;
    }
}

// 调用
const xb = new Dog<string>('小白')

泛型接口

interface Animal{
    say<T>(p: T): T;
}

函数

函数类型声明

const fn: (a: number, b: string) => string;

fn = (x: number, y: string): string => x+y;

// 声明和赋值可以写在一起
const fn: (a: number, b: string) => string = (x: number, y: string): string => x+y;

其实这个声明已经限制函数的结构,所以形参和返回值类型都可以省略,如下

const fn: (a: number, b: string) => string;

fn = (x, y) => x+y;

参数个数保持一致

function test(a: number, b: boolean): string{
    return '111'
}

test(12, true)

可选参数

可选参数 必须放在 必填参数 后面,可选参数使用?

function test(a: number, b?: boolean): string{
    return '111'
}

test(12, true)
test(12)

默认参数

默认参数 必须放在 必填参数 的后面,但和 可选参数 的位置可前可后不规定

function test(a: number, b: number = 10, c?: boolean): string{
    return '111'
}

test(12, 23, true)
test(23, 33)
test(23)

剩余参数

当函数参数个数不确定时,我们可以传入未知个数参数,我们需要通过 rest 参数获取剩余参数

function test(s: boolean, ...args: (string | number)[]): void{
    console.log(123)
}

test(true, 'xxww', 123)

注意:rest参数必须是形参中最后一个

重载

函数名相同,参数(类型、个数)不同,ES原生无此功能,TS新增此功能

// 方式一
function demo(p: string): string
function demo(p: boolean): number

function demo(p: any): any{
    if(typeof p === 'string'){
        return 'wt'
    }
    
    if(typeof p === 'number'){
        return 123
    }
}

const a = demo('wt')  //a:string

// 方式二
function demo(p: string|number): string|number{
    if(typeof p === 'string'){
        return 'wt'
    }
    
    if(typeof p === 'number'){
        return 123
    }
}

const b = demo(12)  //b: string|number

注意:第一种方式相比第二种方式在返回值的类型更加精准