TypeScript语言特性

920 阅读14分钟

概要

Javascript作为一门弱类型的语言,使用简单并且灵活多变。但是,简单灵活的背后却隐藏着许多类型异常的问题,这种隐患在体积越大越复杂的项目中越容易被爆出。为了从语言层面根本性解决这个问题,TypeScript应运而生。这篇专题描述TypeScript作为一门强类型的脚本语言具有哪些优势以及它的诸多特性。

强类型 vs 弱类型

一门语言按照类型安全的角度来看,可以分为强类型和弱类型两种。下面列举二者的特点和区别:

  • 强类型从语言层面限制了函数的实参和形参在数量和类型上必须完全对应,例如foo函数有一个字符串参数,如果使用时给它传入了数字类型,那么在程序编译时就会报出类型异常的错误
  • 弱类型在语言层面不会限制实参类型,如下示例代码:可能期望的是传入数字类型,但是传入字符串类型,程序依然正常运行,只是不符合期望结果
function foo (num) {
    console.log(num + 123);
}

foo(100) // 223
foo('100') // 100223
  • 弱类型语言中允许任意的数据类型转换,隐式类型转换可以说是Javascript这门弱类型语言颇为有趣的特点之一了,虽然灵活多变,却暗中存在隐患
  • 强类型语言中不允许任意的隐式类型转换
  • 强类型和弱类型的区分是从语言层面区分,强类型语言在编译时就可以根据形参和实参类型是否一致决定是否抛出编译异常,而弱类型语言如果想要做类型检查只能在某个具体的方法中通过逻辑判断的方式实现,并且只能在运行时检查,而不是在编译阶段就抛出异常。
  • 对于Javascript,如果某个方法在调用时具有实参形参类型不一致的情况,并且由于参数类型不一致会导致程序错误,那么实际情况是除非这个方法被触发,否则程序永远不会抛出错误,这也就产生了潜在的隐患。
  • 在Javascript中,目前能遇到的类型错误检查都是在代码运行时根据逻辑判断手动抛出的。

静态类型与动态类型

从类型检查的角度来看,一门语言可以被分为静态语言和动态语言。静态语言是指一个变量的类型在声明后不可修改,而动态类型的变量类型只有在运行阶段才能够被明确,而且变量的类型随时可修改。

var x = y ? 1 : 'a';

上面代码中,变量 x 到底是数值还是字符串,取决于另一个变量 y 的值,y 为 true 时,x 是一个数值,y 为 false 时,x 是一个字符串。这意味着,x 的类型无法在编译阶段就知道,必须等到运行时才能明确。

如Javascript这种动态语言的特点可以概况为:动态语言的变量没有类型,而变量中存放的值是有类型的。

为什么Javascript选择成为一门动态语言和静态语言

早期的Javascript应用较为简单,代码可能也就几百行或者几十行。类型检查的优势无法得到体现并且显得不够灵活,并且Javascript作为一门脚本语言,没有编译环节,而类型检查是需要在编译环节去实现的。综合当时的情况考虑,Javascript的设计者选择了更加灵活多变的语言形态。

弱类型的一些缺点

下面使用一些示例代码列举Javascript作为一门弱类型语言的缺陷,相信你平常开发中绝对遇到过这些问题。

① 弱类型产生语法隐患

const foo = {};
const button = document.getElementById('btn');

button.addEventListener('click', () => foo.bar());
// 注意,这里的foo对象压根没有bar方法

上面的实例代码在没有人为的点击button元素时,是不会报错的。也就是Javascript认为以上的代码没有语法错误,这种错误只有在执行阶段被执行到才会触发。(假设,上面的button元素点击行为是一个难以测试到的功能点,那么程序就会隐藏这样一个潜在的错误,而这种错误在强类型语言中在编译阶段就会被抛出)

② 弱类型导致函数功能变质

function add(num1, num2) {
    return num1 + num2;
}

add(100, 100) // 200
add(100, '100') // 100100

上面的add函数原意是想实现两个数字的加法运算,如果在调用add函数数没有传入两个数字类型,函数的功能就完全变味了。(如果在强类型语言中,实参和形参类型不一致在编译阶段就会抛出错误)

③ 无处不在的隐式类型转换

const foo = {};

foo[true] = 'one direction';
// 正常情况下,对象的属性名应该是字符串类型或者是Symbol类型,但是依然可以使用布尔值等其他类型作为对象的属性,原因在于true被自动转换为了String类型

console.log(foo['true']); // 'one direction'

[] == false // true
// 在Javascript的非严格相等运算符两侧,当数据类型不一致时,会发生很有趣的隐式类型转换

'4' - '3' // 1
// 各类运算符如:加减乘除,当运算符两侧的变量类型与期望类型不一致时,运算符两侧就会自动发生隐式类型转换

关于非严格相等运算符的隐式类型转换,可以参考 阮一峰前辈的数据类型转换专题

强类型的优势

① 错误更早暴露

编译阶段可以发现绝大多数的类型异常问题,不必等到程序运行阶段再去处理这种错误

② 代码更智能,编码更准确

Visual Studio等编辑器对强类型语言有更友好的支持度,智能提示和错误追踪效率更高

function render(element) {
    element.innerHTML = '智能提示';
}

上面示例代码中:如果Javascript是一门强类型语言,每个变量都有自己的类型,编辑器就可以根据声明时的类型定义明确知道element是一种Node类型,也就可以智能提示元素标签应有的一些API,但是在这个函数里,element仅仅代表一个变量,没有任何类型区分,所以编辑器无法给出正确的智能提示,我们只能根据记忆中的API去编程

③ 重构更可靠

重构通常是指对原有代码逻辑进行一些破坏性的改动,例如原有API的删除。

比如foo对象下原本有个bar方法,但是在重构后,这个bar方法被改造了并且名称换做了newbar。

那么这种情况下,对于Javascript来说,如果没有执行到bar方法,程序是不会报错的(报错因为bar已被删除)。

而强类型语言有个好处就是编译时就会发现bar不存在的问题,开发者也可以根据提示针对性的解决这个问题。因此强类型语言开发的程序重构会更加友好。

④ 减少不必要的类型判断

对于一个方法来说,如果它依赖的参数类型是非常严苛的,在Javascript中,我们通常会这样做:

function add(num1, num2) {
    if (typeof num1 !== 'number' || typeof num2 !== 'number') {
        throw new TypeError('num1、num2必须都是数字类型');
    }
}

如果使用强类型语言,这种类型判断就完全不需要了,从语言层面就限制了实参和形参的类型必须一致,当我们希望参数都是数字类型时,只需在定义函数时提前申明好参数类型即可。

类型注解

Flow和TypeScript的类型注解

Flow 和 TypeScript 都是 FaceBook 公司开源的实现了类型检查的工具/语言。

不过,Flow 仅仅是一个工具,通过在原有 js 文件上添加注释以声明它是一个使用了 Flow 工具的 js 文件,这种 js 文件最后也还是要被编译为符合 Javascript 语法规范的 js 文件。

TypeScript 则是 Javascript 的超集,它在实现了原有 Javascript 语法规范的基础上额外支持类型系统ES6+,同时 TypeScript 文件以 .ts 格式结尾,需要被编译为 js 才能在浏览器中使用。

虽然二者在存在形式上有着根本的区别,但是对于类型注解,二者的语法非常相似,只是在语法的错误信息提示上可能有些许差别。

类型注解 - 原始类型

  • string: 即 Javascript 中的字符串类型
  • number: 即 Javascript 中的 Number 类型,注意 NaN 和 Infinitiy 也是属于 Number 类型
  • boolean:布尔类型
  • null: 注解为 null 类型的变量只能拥有 null 这一种值
  • void: 即 Javascript 中的 undefined
  • symbol: 该注解类型限定变量必须是 symbol 类型
const foo:string = 'foo';
const foo:number = NaN
const foo:boolean = true
const foo:null = null
const foo:void = undefned
const foo:symbol = Symbol('foo')

类型注解 - 数组类型

数组的类型注解主要有以下三种情形:

// ① 固定成员类型不固定数组长度
const foo:Array<number> = [1,2,3]

// ② 固定成员类型不固定数组长度
const foo:number[] = [1,2,3]

// ③ 固定成员类型及数组长度(元j组)
// 当在函数中需要返回多个数据类型时就可以使用元组
const foo:[string, number] = ['foo', 100]

类型注解 - 对象类型

使用类型注解描述对象解构与对象字面量形式非常类似

// ① 限定对象的成员有且仅有foo和bar属性,并且foo为string类型,bar为number类型

const cac:{foo:string,bar:number} = {foo: '123', bar:123};

// ② 设置某个可选成员属性,下面的实例中设置hh属性为可选属性
const cac:{foo:string,bar:number, hh?:string} = {foo: '123', bar:123}

类型注解 - 函数

对函数的类型限制一般指的就是对函数的参数以及返回值类型做限制

// 1. 限制输入输出的类型
function sum(x: number, y: number): number {
    return x + y
}
// 多余的输入不被允许
sum(1,2,3) // 会报错
// 不足的输入不被允许
sum(1) // 会报错

// 2. 限制回调函数的参数类型及返回值类型
function foo(callback:(a:number, b:number) => number) {
    callback(1, 10);
}

foo(function(foo,bar) {
    console.log(foo + bar);
    return foo + bar;
})

类型注解 - 特殊类型

// ① 字面量类型(或类型、联合类型),如下示例限制了state变量只能是'success、'failed'、'info'中的其中一种
const state: 'success' | 'failed' | 'info' = 'success'

// ② 使用或类型(联合类型)使变量支持多种数据类型
const foo:number | string = 100
const foo:number | string = 'bar'

// ③ 使用type关键词设置(类型)别名
type hash = string | number

const foo:hash = 'qishi'
const foo1:hash = 123

类型注解 - mixed & any

mixed和any可以表示任意类型,但是mixed依然是强类型,而any是弱类型。mixed相当于所有类型的一个联合类型,依然属于强类型

Flow的基本使用

安装 flow-bin 并生成配置文件

cnpm i flow-bin -D

// 生成配置文件
// yarn 会拿着其后面紧跟的命令(flow init)到 node_modules 目录的 bin 目录下去寻找该命令并执行
yarn flow init

书写 flow 语法并给出标志

// 所有需要进行类型检查的 js 文件都需要在文件开头添加[// @flow]

function add(a: number, b: number): number {
    return a + b
}

add(1,'2')

运行 flow 命令执行检查

// 运行flow命令,注意,运行前需要保证项目已生成flow的配置文件
yarn flow

// 如上,sum 函数调用时传入了错误的参数类型,就会给出报错提示

TypeScript 的基本使用

初始化配置文件 tsconfig.json

// 1 在安装TypeScript后,可以使用tsc命令编译单个ts文件,也可以使用tsc命令结合配置文件编译整个使用TypeScript开发的项目

// 2 使用tsc --init可以初始化生成一个配置文件
tsc --init

配置文件tsconfig.json中的lib选项

默认情况下,配置文件的 target 选项是"es5",lib 选项不存在,那么此时无法直接使用 symbol 和 promise,需要给 lib 选项配置标准库。那么如何理解这个 lib 呢?

实际上,标准库就是 Symbol 和 Promise 等内置对象所对应的声明文件。如果需要这些内置对象时,便需要在 lib 选项中指定,该选项是一个数组。

"lib": ["ES2015","DOM"],

TypeScript 作用域的问题

在一个 TypeScript 项目中,在全局作用域下声明的变量不能重复,否则 TypeScript 在编译时会报错。如 src 文件夹下同时包含 01.ts 和 02.ts,如果在01.ts文件中已声明变量 a,那么在 02.ts 再次声明变量 a 就会报错。

解决方案有:给 01.ts 和 02.ts 文件末尾都设置 export {},利用 ES2015 的 ES Module 将其模块化。

// 01.ts
const a:number = 1

export {}

// 02.ts
const a:number = 1

export {}

TypeScript 的 object 类型

object 类型指除了原始类型以外的所有类型

const foo:object = '132' // 编译报错
const foo:object = 100 // 编译报错
const foo:object = {} // 正确编译
const foo:object = [] // 正确编译

用案例感受TypeScript的优势

// sum是一个求和函数

// js版本
function sum(...args) {
    // 这里需要确保args中的每个参数都是数字类型,否则程序运行结果可能不符合预期
    return args.reduce(function(prev, cur){
        return prev + cur;
    }, 0)
}

console.log(sum(1,2,3,4,'5')); // 105

// ts版本
function sum1(...args:number[]) {
    // 从语法上限制了args的类型只能是数字类型的数组
    return args.reduce(function(prev, cur){
        return prev + cur;
    }, 0)
}

console.log(sum(1,2,3,4,5)); // 15
console.log(sum(1,2,3,4,'5')); // 编译时会报错

TypeScript枚举类型

// 最典型的枚举使用,指定每个枚举成员的值
enum PostStatus {
    success = 0,
    info = 1,
    failed = 2
}

const post = {
    title: 'Hello TypeScript',
    content: 'TypeScript',
    status: PostStatus.success
}

console.log(post);
// { title: 'Hello TypeScript', content: 'TypeScript', status: 0 }

// 枚举成员值不指定时,从0开始递增
enum PostStatus {
    success,
    failed,
    info
}

const post = {
    title: 'hash',
    status: PostStatus.failed
}

console.log(post);
// { title: 'hash', status: 1 }

TypeScript隐式类型推断

设置一个变量但是没有指定变量类型时,TypeScript会根据变量定义时的值自动推断变量类型

let a = 100

a = 'str' // 这里会报错

// 声明变量时不指定值,那么该变量就是any类型
let b

b = 100
b = 'foo'

TypeScript接口

接口是强类型语言的一种概念,用来约束对象的结构。在TypeScript中,接口最直观的体现就是可以规定一个对象应该有哪些固定数据类型的成员

// 1 接口的常见使用
interface Post {
    title: string,
    href: string
}

function printPost(post: Post) {
    console.log(post.href);
    console.log(post.title);
}

printPost({
    title: 'hh',
    href: 'dasda'
})

// 2 可选成员和只读成员
interface Foo {
    title: string,
    readonly subtitle: string,
    index?: number
}

function prinitFoo(foo: Foo) {
    foo.title = 'aa';
    // 无法修改只读成员 foo.subtitle = 'bb';
    console.log(foo.index);
}

prinitFoo({
    title: 'aa',
    subtitle: 'b',
    index: 5,
})
// 5

prinitFoo({
    title: 'aa',
    subtitle: 'b',
})
// undefined

// 3 用于缓存对象的接口,因为缓存对象的属性名是无法预知的,因此,不能严格的限制它的属性名称
interface Caches {
    [prop: string]: string
}

const cache: Caches = {}

cache.foo = 'dd'
cache.bar = 'hh'
// cache对象可以保存任意字符串类型的属性名和属性值

类在TypeScript中的基本使用

class Hell {
    title: string;
    num: number;

    constructor(title: string, num: number) {
        this.title = title;
        this.num = num;  
    }

    say(time: string): string {
        return `${time}ddd`
    }

}

类的三种修饰符

  • 类的属性成员默认是public修饰符
  • 当类的属性成员被显式设为private修饰符,成员不可在实例及子类中使用
  • 当类的属性成员被显式设为protected修饰符,成员可以在子类中使用