深度入门TypeScript

大熙哥 2021年11月10日 75次浏览

之前在自研框架的时候,选用TS开发,使用webpack打包,但是最后由于没有系统研究过TS的开发流程和具体内容,还是以JS的思维去写,感觉不到TS的强大。因此本篇系统学习TS。

什么是TypeScript?

  1. 类型系统
  2. 适用于任何规模

类型系统

「类型」是其最核心的特性,JavaScript 是一门弱类型的编程语言。TypeScript 是静态类型,动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。JavaScript 是一门解释型语言,没有编译阶段,所以它是动态类型。
TypeScript 强大的「类型推论」也可以自动推论出它是一个什么类型,从而判断你代码是否正确使用。

我们都听过强语言、弱语言。那么有没有听过强类型和弱类型呢?

类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。,TypeScript 是弱类型。

第一个案例Hello TS

function sayHello(person: string) {
    if (typeof person === 'string') {
        return 'Hello, ' + person
    } else {
        throw new Error('person is not a string')
    }
}

let user = 'TS';
console.log(sayHello(user))

在 TypeScript 中,我们使用 : 指定变量的类型,: 的前后有没有空格都可以。

原始数据类型

JavaScript 的类型分为两种:

  1. 原始数据类型(Primitive data types)
  2. 对象类型(Object types)

原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。本篇只介绍一些特殊的情况。

空值

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

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null。

function alertName(): void {
    alert('My name is Tom')
}

Null 和 Undefined

与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量可以赋值给 number 类型的变量,而 void 类型的变量不能赋值给 number 类型的变量。

let num: number = undefined // 严格模式下,会报错

任意值

任意值(Any)用来表示允许赋值为任意类型。声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。也就是不限制类型。

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型。

但是慎用,everthing is any,ts 基本就是 js,检查变量的时候也不能快速知道类型。

let myFavoriteNumber: string = 'seven'
myFavoriteNumber = 7

类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

let myFavoriteNumber = 'seven'
myFavoriteNumber = 7

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。联合类型使用 | 分隔每个类型。当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。

let myFavoriteNumber: string | number
myFavoriteNumber = 'seven'
myFavoriteNumber = 7

接口

此接口非彼接口,而是对应后端开发的接口Interfaces。

类似于后端开发,需要定义数据库表的实体类,亦或者,对某一种功能通过接口去抽象方法函数和返回值,通过实现接口来完成工作。

interface Person {
    name: string
    age: number
}

let tom: Person = {
    name: 'Tom',
    age: 25
}

可选属性

有时我们希望不要完全匹配一个形状,那么可以用可选属性。

interface Person {
    name: string
    age?: number
}

let tom: Person = {
    name: 'Tom'
}

任意属性

使用 [propName: string] 定义了任意属性取 string 类型的值。

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型,或者可以修改任意属性的类型为 any。

interface Person {
    name: string
    age?: number
    [propName: string]: string
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
}

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性。注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。

interface Person {
    readonly id: number
    name: string
    age?: number
    [propName: string]: any
}

let tom: Person = {
    id: 114514,
    name: 'Tom',
    gender: 'male'
};

tom.id = 1919810;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

数组的类型

在 TypeScript 中,数组类型有多种定义方式。

「类型 + 方括号」

let fibonacci: number[] = [1, 1, 2, 3, 5]

数组泛型

这和java感觉很像,看来我学了一点jvav也是有点用的。

let fibonacci: Array<number> = [1, 1, 2, 3, 5]

接口表示数组

interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5]

类数组

注意例如arguments,不能用普通的数组的方式来描述,而应该用接口。

function sum() {
    let args: {
        [index: number]: number
        length: number
        callee: Function
    } = arguments
}

事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等。

其中 IArguments 是 TypeScript 中定义好了的类型。

interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

任意类型数组

一个比较常见的做法是,用 any 表示数组中允许出现任意类型。

let list: any[] = ['114514', 233, { name: 'xiaoxi' }]

函数的类型

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression)。

函数是一等公民

当我们说函数是“一等公民”的时候,我们实际上说的是它们和其他对象都一样,没什么特殊的,你可以像对待任何其他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量...等等。

const hi = name => `Hi ${name}`
const greeting = name => hi(name)
hi("jonas"); // "Hi jonas"
greeting("times"); // "Hi times"

这种代码主要是体现在ajax回调上。

httpGet('/post/2', json => renderPost(json))

// 可以传递 err 参数。
httpGet('/post/2', (json, err) => renderPost(json, err))

虽说添加一些没有实际用处的间接层实现起来很容易,但这样做除了徒增代码量,提高维护和检索代码的成本外,没有任何用处。
优化的写法:

// renderPost 将会在 httpGet 中调用,想要多少参数都行
httpGet('/post/2', renderPost)

在命名的时候,我们特别容易把自己限定在特定的数据上(本例中是 articles)。这种现象很常见,也是重复造轮子的一大原因。

// 只针对当前的博客
const validArticles = articles =>
  articles.filter(article => article !== null && article !== undefined),

// 对未来的项目更友好
const compact = xs => xs.filter(x => x !== null && x !== undefined)

TypeScript函数声明

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

function sum(x: number, y: number): number {
    return x + y
}
sum(1, 2, 3)
// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

函数表达式

let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};

用接口定义函数的形状

我们也可以使用接口的方式来定义一个函数需要符合的形状。

interface SearchFunc {
    (source: string, subString: string): boolean
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1
}

可选参数

可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了。

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName
    } else {
        return firstName
    }
}
let tomcat = buildName('Tom', 'Cat')
let tom = buildName('Tom')

参数默认值

在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数。此时就不受「可选参数必须接在必需参数后面」的限制。

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName
}
let tomcat = buildName('Tom', 'Cat')
let tom = buildName('Tom')

剩余参数

ES6 中,可以使用 ...rest 的方式获取函数中的剩余参数。事实上,items 是一个数组。所以我们可以用数组的类型来定义它。

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item)
    });
}

let a = []
push(a, 1, 2, 3)

重载

我们可以使用重载定义多个 reverse 的函数类型。这样能够精确的表达,输入为数字的时候,输出也应该为数字。

function reverse(x: number): number
function reverse(x: string): string
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''))
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('')
    }
}

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。建议大家在使用类型断言时,统一使用 值 as 类型 这样的语法。
用类型断言,通过判断是否存在 code 属性,来判断传入的参数是不是 ApiError。

interface ApiError extends Error {
    code: number
}
interface HttpError extends Error {
    statusCode: number
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true
    }
    return false
}

我们需要将 window 上添加一个属性 foo,但 TypeScript 编译时会报错,提示我们 window 上不存在 foo 属性。

window.foo = 1;

// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'

此时我们可以使用 as any 临时将 window 断言为 any 类型。

(window as any).foo = 1

总结:

  1. 联合类型可以被断言为其中一个类型
  2. 父类可以被断言为子类
  3. 任何类型都可以被断言为 any
  4. any 可以被断言为任何类型

声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

新语法

  1. declare var 声明全局变量
  2. declare function 声明全局方法
  3. declare class 声明全局类
  4. declare enum 声明全局枚举类型
  5. declare namespace 声明(含有子属性的)全局对象
  6. interface 和 type 声明全局类型
  7. export 导出变量
  8. export namespace 导出(含有子属性的)对象
  9. export default ES6 默认导出
  10. export = commonjs 导出模块
  11. export as namespace UMD 库声明全局变量
  12. declare global 扩展全局变量
  13. declare module 扩展模块
  14. /// 三斜线指令

本文参考于 xcatliu 的 TypeScript入门教程