浅谈 TypeScript【下】-- TypeScript 语言规范与基本应用

文章内容输出来源:拉勾教育 大前端高薪训练营

前言

在 【浅谈 TypeScript【上】】中,简单讲述了关于JavaScript静态类型检查工具Flow的用法等。可以看到,我们接下来讲述的TypeScript与它其实有很多相似之处。

TS 与 JS

TypeScript 并不是一个完全新的语言, 它是 JavaScript 的超集,为 JavaScript 的生态增加了类型机制,并最终将代码编译为纯粹的 JavaScript 代码。

TypeScriptJavaScript
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页。
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口
支持 ES3,ES4,ES5 和 ES6 等不支持编译其他 ES3,ES4,ES5 或 ES6 功能
社区的支持仍在增长,而且还不是很大大量的社区支持以及大量文档和解决问题的支持

TypeScript 概述

首先,我们先来看一下 TypeScript的范围图:

TypeScript 范围展示

TypeScript 是由微软开发的一种开源、跨平台的编程语言,是JavaScript 的超集,主要提供了类型系统和对 ES6 的支持,最终会被编译为JavaScript代码。可以说,TypeScript是前端领域中的第二语言,属于渐进式语言。

一、定义

引用官网定义:

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open source.

中文意思:

TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。

二、特点

  1. 自动转换新特性,最低可以编译到ES3版本;
  2. 任何一种 JavaScript 运行环境都支持;
  3. 相比于Flow,功能更为强大,生态也更健全、更完善。

三、TypeScript的优势和缺点

TypeScript的优势

  1. TypeScript 增加了代码的可读性和可维护性
    a. 类型系统可以方便用户快速的使用类型定义过后的函数变量;
    b. 在编译阶段发现错误,节省了运行之后出错的维护时间;
    c. 增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等。

  2. TypeScript 非常包容
    a. TypeScript 是 JavaScript 的超集,.js 文件可以直接重命名为 .ts 即可;
    b. 可以根据变量的值等进行类型推论,可以定义从简单到复杂的几乎一切类型;
    c. TypeScript 编译报错,不会影响生成 JavaScript 文件;
    d. 兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取。

  3. TypeScript 拥有活跃的社区
    a. 大部分第三方库都有提供给 TypeScript 的类型定义文件;
    b. Google 开发的 Angular2 就是使用 TypeScript 编写的;
    c. TypeScript 拥抱了 ES6 规范,也支持部分 ESNext 草案的规范。

TypeScript的缺点

  1. TypeScript 增加了接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师不熟悉的概念,需要一定的学习成本;
  2. 项目初期,TypeScript 会增加一些开发成本,需要去编写一些类型的定义。但在后期维护时,会减少维护成本。
  3. 集成到构建流程需要一些工作量;
  4. 可能和一些库结合的不是很完美。

快速上手

  • 以下操作需在 cmd 命令行界面进行,并使用 yarn (或 npm/cnpm)执行安装运行命令。
  1. 初始化包管理文件 package.json
    yarn init --yes  // --yes 表示快速初始化
    
  2. 安装 TypeScript
    yarn add typescript --dev   // --dev 表示项目内安装,而非全局安装
    
  3. 新建 .ts 结尾的 hello.ts文件,使用 tsc 命令将 .ts文件编译为 .js文件
    yarn tsc hello.ts    // 进入到当前目录,否则需加上文件路径
    
  • 解析

    编译过程中,先检查类型是否使用异常,然后移除一些类型注解等扩展语法,并且会自动转换 ECMAScript 的新特性。

配置文件

  • 在项目根目录进行操作, tsconfig.json 配置属性将简单介绍。
  1. 初始化 tsconfig.json 配置文件

    yarn tsc --init
    
  2. tsconfig.json 配置属性

    {
      "compilerOptions": {  
        "target": "es5",          /* Specify ECMAScript target version */
        "module": "commonjs",     /* Specify module code generation */
        "sourceMap": true, /* Generates corresponding '.map' file. */
        "outDir": "dist",  /* Redirect output structure to the directory. */                
        "rootDir": "src",  /* Specify the root directory of input files. */             
        "strict": true,    /* Enable all strict type-checking options. */                     
        "esModuleInterop": true,            
        "skipLibCheck": true,                   
        "forceConsistentCasingInFileNames": true  
      }
    }
    

    – rootDir:编译源代码,一般在 src 文件夹中存放
    – outDir:输出文件,一般放置在 dist 文件夹中

  3. 根据配置文件,编译 src 中全部的 .ts 文件

    yarn tsc
    

基础语法

原始数据类型

在JavaScript中,原始数据类型分别为:数值类型、字符串类型、布尔类型、Null、Undefined以及ES6中新增的Symbol。

字符串类型

使用 string 定义字符串类型:

const a: string = 'foobar'

编译结果:

"use strict";        // 配置文件中,配置了严格模式为 true
var a = 'foobar';

数值类型

使用 number 定义数值类型:

const num: number = 100 
const na: number = NaN 
const infi: number = Infinity

编译结果:

var num = 100; 
var na = NaN; 
var infi = Infinity;

布尔类型

使用 boolean 定义布尔值类型:

const t: boolean = true 
const f: boolean = false

编译结果:

var t = true;
var f = false;

空值

JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

function voidTest(): void {
    // 函数体
}

声明变量的数据类型为 void 时,非严格模式下,变量的值可以为 undefined 或 null。而严格模式下,变量的值只能为 undefined。

const u: void = undefined 
const t: void = null   // 严格模式下,语法报错

Null 与 Undefined

使用 nullundefined 来定义 null 和 undefined 这两个原始数据类型:

const f: null = null
const g: undefined = undefined

编译结果:

var f = null;
var g = undefined;

注意

void类型的变量不能赋值给 number 、string、boolean类型的变量。

let u: void;
let num1: number = u;
// Type 'void' is not assignable to type 'number'.

严格模式下,null、undefined类型的变量不能赋值给 number 、string、boolean类型的变量。

let num2: number = undefined;
// Type 'undefined' is not assignable to type 'number'.

let num3: number = null;
// Type 'null' is not assignable to type 'number'.

Symbol

Symbol 是ES2015中新增的数据类型。因此,我们需要做一些操作,使程序在编译过程中可以识别 ES2015中的新特性。

解决方案

  • 方案一:
    将 tsconfig.json 文件中的 “target”,设为 “es2015” 以上版本。

  • 方案二:
    tsconfig.json 文件中的 “target” 还是为 “es5"等原来的版本,但要添加标准库(标准库就是内置对象所对应的声明),即在 tsconfig.json 文件中的 “lib” 数组中添加 “ES2015”,但是若只添加这一选项,则会覆盖原来的标准库,因此需要把原来的标准库添加回来,如"DOM”(在标准库中,"DOM"标准库包含了 dom 和bom)

const h: symbol = Symbol() 

Object 类型

  • 不是特指对象类型,而是泛指所有的非原始的数据类型,即包括 对象、数组、函数 等。
const foo: object = function () {} 
const bar: object = [] 
const baz: object = {}

// 类似对象字面量:属性类型数量要一一对应
const obj: { foo: number, bar: string } =  { foo: 123, bar: 'string' }

数组类型

声明数组的两种形式:

  • 使用 Array泛型
const arr1: Array<number> = [1, 2, 3] 
  • 使用 数据类型 + [] 形式,这种比较常用
const arr2: number[] = [1, 2, 3] 

// 数据类型 + [], 常用的举例
function sum (...args: number[]) {
    return args.reduce((prev, current) => prev + current, 0)
}
// sum(1, 2, 3, 'foo') // 报错

元组类型(Tuple Types)

元组就是一个明确元素数量以及元素类型的数组。各个元素的类型不必要完全相同。一般用于在一个函数中去返回多个返回值。

定义元组

  • 使用类似 数组字面量 的形式,如果元素对应类型不相符,或者元素数量不一致,都会报错。
const tuple: [number, string] = [18, 'foo']

访问的两种形式

  • 使用数组下标的形式
const age = tuple[0]
const name = tuple[1]
  • 使用数组解构的方式,提取数组中的每个元素
const [age, name] = tuple

// 返回元组的例子
Object.entries({
    foo: 123,
    bar: 456
})

枚举类型

特点
1、给一组数值去分别取上一个更好理解的名字;
2、一个枚举中只会存在几个固定的值,并不会出现超出范围的可能性。

语法和注意

  • js语法:使用对象模拟实现枚举
const PostStatus = {
    Draft: 0,
    Unpublished: 1,
    Published: 2
}
  • TypeScript语法:使用 enum 关键字, 注意使用的是 “=”
const PostStatus = {
    Draft = 0,
    Unpublished = 1,
    Published = 2
}	
  • 可以不指定具体的值,默认从 0 开始累加
enum PostStatus {
    Draft,
    Unpublished,
    Published
}	
  • 也可以给第一个指定具体的值,后面的值将会在第一个指定值的基础上,执行累加
enum PostStatus {
    Draft = 3,
    Unpublished,
    Published
}	
  • 给定的值,既可以是数值型,也可以是字符串,即字符串枚举
// 由于字符串无法累加,因此需要自行赋值。字符串枚举并不常见
enum PostStatus {
    Draft = 'aaa',
    Unpublished = 'bbb',
    Published = 'ccc'
}	
// PostStatus[0] // =>Draft 通过索引器的形式访问对应的枚举名称

枚举类型会影响编译的结果,最终会编译为双向的键值对对象。
编译结果:

 // 目的:可以让我们动态的通过枚举值(0, 1, 2, ...)去获取枚举的名称
var PostStatus;
(function (PostStatus) {
    PostStatus["Draft"] = "aaa";
    PostStatus["Unpublished"] = "bbb";
    PostStatus["Published"] = "ccc";
})(PostStatus || (PostStatus = {}));
  • 如果确定不会使用索引器的形式访问枚举,那么建议使用常量枚举。
const enum PostStatus {
    Draft,
    Unpublished,
    Published
}
const post = {
    title: 'Hello TypeScript',
    content: 'TypeScript is a typed superset of JavaScript.',
    status: PostStatus.Draft // 2 // 1 // 0
}

函数类型

对函数的输入输出进行类型限制,输入:参数;输出:返回值。

定义函数的方式

  • 方式一:函数声明
/**  
 * 如果某个参数可传可不传:
 *    1、可在形参的参数名后面添加 "?" , 使其变成可选的;
 *    2、使用 es6中添加参数默认值的方法, 使其变成可有可无的。
 * 
 * 若需要接收任意个数的参数,使用 es6 中的rest操作符
 * 
 */

function func1 (a: number, b?: number, c: number = 10, ...rest: number[]): string {
    return 'func1'
}

func1(100, 200)

注意
1)形参列表都为必传参数时,传入的实参类型和数量,都必须与形参保持一致;
2)可选参数,必须放在参数列表的最后面。

  • 方式二:函数表达式
    这种对于回调函数的形参类型,需要进行约束。
const fun2: (a: number, b: number) => string = function (a: number, b: number): string {
    return 'func2'
}

任意类型

any 就是用来接收任意类型数据的一种类型,属于动态类型,不会有任何的类型检查,很可能会存在类型安全的问题,轻易不要使用。

function stringify (value: any) {
    return JSON.stringify(value)
}

stringify('string')
stringify(100)
stringify(true)

let foo: any = 'string'
foo = 100 // 在使用过程中,可以接收任意类型的数据

foo.bar() // 语法上不会报错

类型断言

类型断言是在编译过程中的概念,代码编译过后,将不会存在。而类型转换是代码运行时的概念。

// 假设这个 nums 来自一个明确的接口
const nums = [110, 120, 119, 112]
const res = nums.find(i => i > 0)

// TypeScript 推断出 res: number | undefined,不能执行下面类似操作
const square = res * res    // 报错,不可以执行

类型断言的两种方式

  • 使用 as 关键字,明确告诉 TypeScript 这个数据的具体类型
const num1 = res as number
  • 使用 <数据类型> 的方式,但 JSX下会有语法冲突,不能使用
const num2 = <number>res 

接口(Interfaces)

什么是接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

作用
接口就是用来约束对象的结构。一个对象要是去实现一个接口,那么这个对象必须拥有接口中的所有成员。

语法

interface  接口名称 {
    属性名1: 数据类型;
    属性名2: 数据类型
    ...
    属性名n: 数据类型
}

代码示例如下:

// 约定一个对象当中,具体应该有哪些成员,以及成员的类型又是什么样的
 interface Post {
     title: string;           // 内部成员可用 ";" 分割,也可以不加 
     content: string
     subtitle?: string        // ? 可选成员
     readonly summary: string // 只读成员
 }
  
 function pointPost (post: Post) {
     console.log(post.title);
     console.log(post.content);   
 }
  
const post: Post = {
    title: 'Hello TypeScript',
    content: 'A javascript superset',
    summary: 'A javascript'
}
// post.summary = 'other' // 不可修改
pointPost(post)
    
// 添加动态成员
interface Cache {
    [key: string]: string  // key 代表属性名,可随意取名
}

const cache: Cache = {}

cache.foo = 'value1'
cache.bar = 'value2'

总结
TypeScript 中的接口只是用来为有结构的数据,做类型约束。在实际运行中,没有实际意义。

类(class)

概述
描述一类具体事物的抽象特征,主要用来描述一类具体对象的抽象成员。

声明属性
在TypeScript中要明确,在类型当中声明它所拥有的一些属性,而不是在构造函数当中动态通过this去添加。

/**
 * ES2016新增,在类型当中声明属性的方式,就是直接在类中定义
 *
 * 注意:在TypeScript中,类的属性必须有一个初始值
 * 
 * 属性赋初始值的方式:
 *    1、在 "=" 后面赋值
 *    2、在构造函数中进行初始化,动态的为属性赋值
 */
class Person {
    name: string    // = 'init name' 
    age: number 

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

    sayHi (msg: string): void {
        console.log(`I am ${this.name}, ${msg}`)        
    }
}

类的访问修饰符

  • 作用:
    主要用来控制类当中成员的可访问级别。
  • 分类
    1)private :私有成员,只能在类的内部进行使用,外部访问不到,不允许继承。
    2)public : 公有成员,默认就是 public,建议手动添加上,便于理解。
    3)protected : 受保护的,外部访问不到,只允许在子类当中访问对应的成员,允许继承。
  class Person {
      public name: string // 公有属性
      private age: number  // 私有属性
      protected gender: boolean // 受保护的,只能在子类中被访问
  
      constructor (name: string, age: number) {
          this.name = name
          this.age = age
          this.gender = true
      }
  
      sayHi (msg: string): void {
          console.log(`I am ${this.name}, ${msg}`)        
          console.log(this.age)        
      }
  }
  const tom = new Person('tom', 18)
  console.log(tom.name)
  // console.log(tom.age)    // 私有属性,外部访问不到,报错
  // console.log(tom.gender) // 受保护的属性,只能在子类中访问,报错

构造函数被私有化时,则会发生以下问题:

1)构造函数被私有化,将不能在外部使用 new 关键字进行实例化;
2)需要在类中定义静态方法,并返回 创建的类的实例。

 class Student extends Person {
     private constructor (name: string, age: number) {
         super(name, age)
         console.log(this.gender);
     }
 
     static create (name: string, age: number) {
         return new Student(name, age)
     }
 }
 const jack = Student.create('jack', 20)

类的只读属性

class Person {
    protected readonly gender: boolean // readonly 要放在访问修饰符的后面
    constructor () {
        this.gender = true             // 注意:初始化过后,不可以再修改
    }
}

类 与 接口

// 一个接口只去约束一个能力,让一个类型同时去实现多个接口
interface Eat {
    eat (food: string): void
}
interface Run {
    run (distance: number): void
}

// 不同的类型,实现相同的接口
class Person implements Eat, Run {
    eat (food: string): void {
        console.log(`优雅的进餐:${food}`)        
    }

    run (distance: number) {
        console.log(`直立行走:${distance}`)
    }
}

class Animal implements Eat, Run {
    eat (food: string): void {
        console.log(`呼噜呼噜的吃:${food}`);        
    }

    run (distance: number) {
        console.log(`爬行:${distance}`)        
    }
}

抽象类

类似于接口,用来约束子类当中必须要有某一个成员。但与接口不同的是,抽象类可以包含一些具体的实现,而接口只能是某一个成员的抽象,不包括具有的实现。

// 抽象类只能被继承,不能再使用 new 进行实例化
abstract class Animal {
    eat (food: string): void {
        console.log(`呼噜呼噜的吃:${food}`)        
    }

    // 抽象方法, 不需要方法体
    abstract run (distance: number): void 
}

// 当父类中存在抽象方法时,子类必须去实现抽象方法
class Dog extends Animal {
    run(distance: number): void {
        console.log('四脚爬行', distance);
    }

}

// 子类创建实例时,会同时拥有父类中的方法,以及自身所实现的方法
const d = new Dog()
d.eat('嗯')
d.run(1)

泛型

概述
泛型就是在声明函数时不去指定具体的类型,等到在调用的时候再去传递具体的类型。

目的
极大程度的去复用代码。

function createNumberArray (length: number, value: number): number[] {
    // Array 默认创建的是 any类型的数组,因此需要使用泛型进行指定,传递一个类型
    return Array<number>(length).fill(value)
}

function createStringArray (length: number, value: string): string[] {
    return Array<string>(length).fill(value)
}

// 不明确的类型,使用 T 替换,调用时传入
function createArray<T> (length: number, value: T): T[] {
    return Array<T>(length).fill(value)
}

// const res = createNumberArray(3, 100) // res => [100, 100, 100]

const res = createArray<string>(3, 'foo')

总结
泛型就是定义时不能明确的类型变成一个参数,让我们在使用的时候再去传递的类型参数。

类型声明

作用
兼容普通的js代码。

下面以 lodash 为例,手动声明函数类型

// 此时,只是安装了 'lodash' 开发依赖
import { camelCase } from 'lodash'

// 需要使用 declare 关键字,手动声明函数类型
declare function camelCase(input: string): string

const res = camelCase('hello typed') // 不手动声明,会报错      
  • 安装类型声明模块
yarn add @types/lodash --dev

代码如下(示例):

import { camelCase } from 'lodash'
 
const res = camelCase('hello typed')

类型声明模块介绍
类型声明模块没有实际的代码,只是对对应的模块做一些类型声明。若模块中已经存在类型声明,则不需要引入类型声明模块。

@types/模块名   // 形式

作用域问题

在其他文件中,变量或函数等已经在 全局作用域 中定义,再进行定义的话,就会产生命名冲突,编译时会报错。

解决方案

  • 使用 匿名函数,立即执行,生成单独的作用域
(function () {
    const a = 123
})()
  • 使用 export {},利用导出模块的概念,生成作用域
// 下面的 {} 并不是指导出空对象,而是 export 的语法
// 利用下面的语法,可以使这个文件中的成员变成这个模块当中的局部成员
// 一般不会用,实际开发中,就会以模块的形式进行开发
export {}

const a = 123

Polyfill

TS 对于语言的编译,只是语法层面,如果是 API 层面的的补充,需要手动 Polyfill!

core-js

core-js 基本上把能 polyfill API 都实现了。但是,属于手动引入 Polyfill。

  • 1,安装插件模块

      $ npm i core-js --save
    
  • 2,使用core-js,两种引入方式,建议按需引入

    引入语法如下:

      // 一, 全部引入
      import 'core-js'
      
      // 二, 按需引入
      import 'core-js/features/object'
    
  • 3,测试是否成功

      $ nvm use 12  # 12 node version, compile ts
      $ tsc         # compile ts
      $ nvm use 0   # 0 node version
      $ node xxx.ts # success or failure
    
  • 无法实现 Polyfill

    Object.defineProperty 完全无法 Polyfill
    Promise 微任务,用宏任务代替

Babel

使用 Babel 自动化的 Polyfill。

  • 1,安装依赖模块

      $ npm i @babel/cli @babel/core @babel/preset-env @babel/preset-typescript --save
    

    模块注释

    @babel/cli:babel 的命令行入口
    @babel/core:babel的核心模块
    @babel/preset-env:是一个包含ES新特性所有转换插件的集合,可以根据环境判断哪些转哪些不转
    @babel/preset-typescript:是一个包含typescript转换为ES的插件集合

  • 2,配置babel.config.js文件

    配置代码如下:

      // JSDoc
      
      // @ts-check
      
      /** @type {import('@babel/core').ConfigAPI} */
      module.exports = {
        presets: [
          [
            '@babel/env',
            {
              useBuiltIns: 'usage',
              corejs: {
                version: 3
              }
            }
          ],
          '@babel/typescript' // 不会做 TS 语法检查,只是移除类型注解
        ]
      }
    
  • 3,使用编译命令

      $ npx babel source.ts -o output.js # source.ts 源文件  output.js 编译后的文件
    
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值