typescript入门及实践

typescript越来越成为前端最主流的开发语言,在团队中推行以及升级公司项目迫在眉睫。本篇文章我整理了简单易读却又颇为全面的typescript
入门文档,以及现有项目的迁移方案。

介绍

typescript是在静态编译,而非运行时编译。如果只是类型错误,还是可以照常编译,并且编译结果也是可以运行的。typescript只是做了类型检测,而非逻辑检测。当然,typescript也可以进行一些简单的类型推导。

typescript中文网存在文档更新不及时的问题,建议对照着英文官网学习

基本类型

typescipt中包含javascript中的所有类型,为了便于使用,又新增了几个类型。

typescriptjavascript的超集,即后者有的前者都有。
本文如无特殊说明,javascript中有的特性和问题,typesript都有。

typescript中包含以下几种基本类型:Boolean Number String Undefined Null Symbol Array Object | Tuple Enum Any Void Never

值类型

基本的值类型包括 Boolean Number String Undefined Null Symbol

1
2
3
4
5
6
7
8
9
10
11
let isTrue: boolean = false

let age: number = 20

let name: string = 'zhang san'

let bankAccount: undefined = undefined

let ferrari: null = null

let flag: symbol = Symbol('star')

写法很简单,在变量名后面跟上类型即可。类型名称都是小写,如果赋值与类型不符,则会报错。

注意

  1. 非严格模式下,Undefined Null是所有类型的子类型,即下面写法是不会报错了
1
2
3
4
5
6
7
8

let age: number = 20

age = undefined

let isTrue: boolean = false

isTrue = null

但是,实际开发项目中,严格模式都是开启的,以避免不必要的错误。但项目中,确实会存在值可能为undefined的情况,这时候,我们可以使用联合类型:

1
2
3
4
5
let age: number | undefined

age = undefined

age = 20

上面提到的严格模式,并非是javascript中的严格模式('user strict;')。而是typescript在编译时候的配置项。
在项目配置文件tsconfig.json中,{ compilerOptions: { strict: true }}

Array

数组有两种表示方式:

1
2
3
4

let list: number[] = [2,3,4]

let list: Array<number> = [2, 3, 4]

Array可以理解为内置的泛型

Object

object表示引用类型,即所有的非值类型。

1
2
3
4
5
6
7
8
9
let x: object

x= {a: 2}

x = function (a: string) {
return a
}

x = [1, 2]

Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。其实就是一个限制了长度,确定了元素顺序和单个元素类型的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

let tuple: [string, number]

// 正确写法
tuple = ['a', 2]

// ❌ 顺序错误
tuple = [2 ,'a']

// ❌ 缺少元素
tuple = ['a']

// ❌ 多出一个元素
tuple = ['a', 2, 3]

// ❌ 越界,访问了边界之外的元素
tuple[3]

// 可以添加元素。虽然没有报错,但是毫无意义
tuple.push(4)

// ❌ 只能添加已有类型的联合类型元素。在本例中是`string | number`
tuple.push(true)

// ts只在编译时检测,最多会类型推导。代码逻辑造成的类型错误是无法检测到的
tuple.shift()

let none :number = tuple[1]

console.log(none) // undefined

Any

顾名思义,此类型可以用来表示所有可能的类型,如果你不知道某个变量的类型,设置为any是最稳妥的。

当你从 JavaScript 迁移至 TypeScript 时,你将会经常性使用 any。但你必须减少对它的依赖,因为你需要确保类型安全。当使用 any 时,你基本上是在告诉 TypeScript 编辑器不要进行任何的类型检查。

Object并不相同

js中,所有的变量类型其实都是Object(注意:不是object)。都会拥有Object类型的几个基础属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
interface Object {
/** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
constructor: Function;

/** Returns a string representation of an object. */
toString(): string;

/** Returns a date converted to a string using the current locale. */
toLocaleString(): string;

/** Returns the primitive value of the specified object. */
valueOf(): Object;

/**
* Determines whether an object has a property with the specified name.
* @param v A property name.
*/
hasOwnProperty(v: PropertyKey): boolean;

/**
* Determines whether an object exists in another object's prototype chain.
* @param v Another object whose prototype chain is to be checked.
*/
isPrototypeOf(v: Object): boolean;

/**
* Determines whether a specified property is enumerable.
* @param v A property name.
*/
propertyIsEnumerable(v: PropertyKey): boolean;
}

所以,就有人觉得Objectany没区别啊,反正每个变量都是属于这两个类型的。其实,他们是不同的。有些变量不但属于Object,也属于其他类型,并拥有其他类型的独有方法。比如:

1
2
3
4
5
6
7
8

let imNumber: any = 4

imNumber.toFixed()

let imNumber2: Object = 4

imNumber2.toFixed() // ❌ 类型“Object”上不存在属性“toFixed”

所以,其实Object作为一个「基础类型」,只适用于对象:

1
2
let obj: Object = {}
let obj2: Object = new Object()

注意,它跟「非值类型」-object类型是不同的。

Void

从某种程度上来说,void类型与any类型完全相反,它表示没有类型。它最常用的用途就是表示一个没有返回值的函数:

1
2
3
4
5
6
7
8

let foo = (): void => {
console.log('nothing return')
}

let foo2 = (): string => {
return 'im return string'
}

另外,函数声明时,要么返回值,要么声明返回值为any或者void,换言之,没有返回值函数的返回值类型不能声明为null或者undefined

1
2
3
4
5
6
7
8

let run = (): null => {
console.log(2)
} // ❌ 其声明类型不为 "void" 或 "any" 的函数必须返回值。

let run2 = (): any => {
console.log(2)
}

除此之外,你还可以什么声明一个void类型的变量,但它只能被赋值为undefinednull。 所以,正式开发中,并没什么用。

1
2
3
4
5
6

let vv: void = undefined

let vv2: void = null

let vv3: void = 3 // ❌ 不能将类型“3”分配给类型“void”

Never

never类型表示永不存在的值的类型。一般也是用在函数返回值,只不过是表示永远不会返回的返回值类型。

1
2
3
4
5
6
7
8
9
10
11
12

let imNever = (): never => {
// 抛出错误,永远不会执行完
throw new Error('err')
}

let imNever2 = (): never => {
// 永远在循环里
while(true){
console.log('ok')
}
}

下面这个例子,其实就是说明了为什么要创建never这个类型,因为它确实是程序中的一部分。

1
2
3
4
5
6
7
8
9
10
11
function fail(message: string): never {
throw new Error(message);
}

let imNever3 = (val: number) => {
if (typeof val === 'number') {
console.log('ok')
} else {
fail('Unexhaustive')
}
}

理论上,你永远无法到达else,但是如果到达了,应该就是程序出错了。你应该处理这个错误。

程序语言的设计应该存在一个底层类型的概念,当你在分析代码的时候,这会是一个理所当然存在的类型。
TypeScript 就是这样一种分析代码的语言,因此它需要一个可靠的,代表永远不会发生的类型。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

let imNever4 = (val: number): number => {
if (typeof val === 'number') {
throw new Error('ok')
} else {
return val
}
}

let x: any = 3
let nt:never = x // ❌ 不能将类型“any”分配给类型“never”


let nt1:never = (() => {
throw new Error()
})()

其他用法

Except better type safety (as in cases described above), never type has another use case - conditional types. With never type we can exclude some undesired types:

1
2
3
4
5

type NonNullable<T> = T extends null | undefined ? never : T;

type A = NonNullable<boolean>; // boolean
type B = NonNullable<number | null>; // number

类型推导

虽然ts是在编译时进行检查,即它不会运行你的代码,但是它有类型推导的能力。比如下面这种明显可以走到终点的函数,它是会报错的

1
2
3
4
5
6
7
let never = (): never => {
console.log(2134)
} // ❌ 返回“从不”的函数不能具有可访问的终结点

let never2 = (): never => {
return 'ok'
} // ❌ 不能将类型“"ok"”分配给类型“never”

与void不同

用在函数返回值时,void表示的是返回值为空(undefined 或者 null),而never则是永远不会有返回值的类型–因为程序无法运行到那一步。

Enum

这是typescript中新增的一个类型,可以为一组数值赋予友好的名字。

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。

我们看一个见单的例子:

原代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 支付方式映射
export function paymentTypeFilter (type) {
let statusStr = ''
switch (type) {
case 1:
statusStr = '微信'
break
case 2:
statusStr = '支付宝'
break
default:
statusStr = '未知方式'
}
return statusStr
}

ts代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum PaymentTypes {
// 未知
Default,
// 支付宝
AliPay,
// 微信
WCPay
}

function paymentTypeFilter (type: PaymentTypes) {
let statusStr = ''
switch (type) {
case PaymentTypes.WCPay:
statusStr = '微信'
break
case PaymentTypes.AliPay:
statusStr = '支付宝'
break
default:
statusStr = '未知方式'
}
return statusStr
}

paymentTypeFilter('a') // ❌ 类型“"a"”的参数不能赋给类型“PaymentTypes”的参数
paymentTypeFilter(1) // 限制了传入类型,避免错误

编译后代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var PaymentTypes;
(function (PaymentTypes) {
// 未知
PaymentTypes[PaymentTypes["Default"] = 0] = "Default";
// 支付宝
PaymentTypes[PaymentTypes["AliPay"] = 1] = "AliPay";
// 微信
PaymentTypes[PaymentTypes["WCPay"] = 2] = "WCPay";
})(PaymentTypes || (PaymentTypes = {}));
function paymentTypeFilter(type) {
var statusStr = '';
switch (type) {
case PaymentTypes.WCPay:
statusStr = '微信';
break;
case PaymentTypes.AliPay:
statusStr = '支付宝';
break;
default:
statusStr = '未知方式';
}
return statusStr;
}
paymentTypeFilter('a'); // 虽然编译报错,但依然编译成功
paymentTypeFilter(1);
1
2
PaymentTypes.AliPay // 1
PaymentTypes[1] // "AliPay"

我们看到,枚举值会自动累加计数。可以语义化表达一个值。

你甚至可以这样:

1
2
3
4
5
6
7
8
9
10
11
enum PaymentTypes {
'未知方式',
'支付宝',
'微信'
}

function paymentTypeFilter (type: PaymentTypes = 0): string {
return PaymentTypes[type]
}

console.log(paymentTypeFilter(2))
1
2
3
4
5
6
7
8
9
10
11
var PaymentTypes;
(function (PaymentTypes) {
PaymentTypes[PaymentTypes["\u672A\u77E5\u65B9\u5F0F"] = 0] = "\u672A\u77E5\u65B9\u5F0F";
PaymentTypes[PaymentTypes["\u652F\u4ED8\u5B9D"] = 1] = "\u652F\u4ED8\u5B9D";
PaymentTypes[PaymentTypes["\u5FAE\u4FE1"] = 2] = "\u5FAE\u4FE1";
})(PaymentTypes || (PaymentTypes = {}));
function paymentTypeFilter(type) {
if (type === void 0) { type = 0; }
return PaymentTypes[type];
}
console.log(paymentTypeFilter(2)); // 微信

当然,枚举值也可以为字符串。

1
2
3
4
5
enum Fruits {
A = 'apple',
B = 'banana',
C // ❌ 枚举成员必须具有初始化表达
}
1
2
3
4
5
6
var Fruits;
(function (Fruits) {
Fruits["A"] = "apple";
Fruits["B"] = "banana";
Fruits[Fruits["C"] = void 0] = "C";
})(Fruits || (Fruits = {}));
1
2
3
4
5
6

Fruits.A // apple
Fruits.apple // undefined

Fruits.C // undefined
Fruits["undefined"] // C

const枚举

为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const enum PaymentTypes {
// 未知
Default,
// 支付宝
AliPay,
// 微信
WCPay
}

function paymentTypeFilter (type: PaymentTypes) {
let statusStr = ''
switch (type) {
case PaymentTypes.WCPay:
statusStr = '微信'
break
case PaymentTypes.AliPay:
statusStr = '支付宝'
break
default:
statusStr = '未知方式'
}
return statusStr
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function paymentTypeFilter(type) {
var statusStr = '';
switch (type) {
case 2 /* WCPay */:
statusStr = '微信';
break;
case 1 /* AliPay */:
statusStr = '支付宝';
break;
default:
statusStr = '未知方式';
}
return statusStr;
}

接口

声明

预设声明

typescriptjs的内置变量进行了预先声明,你也可以在配置中添加新的仓库,比如在编译选项(tsconfig.json)中设置DOM,那么DOM所有的内置接口都可以访问。

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"lib": [
"esnext",
"dom"
]
}
}

举个例子,我们在之前「基本类型」中提到的Object

泛型

编译配置

迁移

安装依赖包

1
2

npm i -D typescript @vue/cli-plugin-typescript @vue/eslint-config-typescript
  • typescript

    编译的核心包

  • @vue/cli-plugin-typescript

    包的合集。主要包含ts-lintts-loaderwebpack,以及支持vuevue-class-componentvue-property-decorator

  • @vue/eslint-config-typescript

    包的合集。包含解析tsjs的包,调用eslint进行lint的包。

添加配置文件

  1. tsconfig.json

    用于配置ts的编译范围和编译选项等

  2. tslint.json

    配置lint的选项

参考

  1. typescript官网
  2. typescript中文网
  3. typescript官方仓库
  4. TypeScript Deep Dive
  5. 深入理解 TypeScript
  6. Use of never keyword in typescript
  7. TypeSearch
  8. enum 和 const enum 之间的区别是什么?