likes
comments
collection
share

看完这篇还敢说不会TypeScript吗?

作者站长头像
站长
· 阅读数 18

一篇长文总结 TypeScript 需要掌握的基础,希望对正在学或者想学 TypeScript 的看完这篇文章后的你,会对 TypeScript 有一个初步的理解。

本文偏长,点赞👍获取一块记忆面包

看完这篇还敢说不会TypeScript吗?

1.为什么选择 TypeScript?

  • 类型系统(所有的函数和变量都有定义的类型)
  • 静态检查(类型注解提供编译时的静态类型检查,减少编译时才找到bug的痛苦)
  • 文档清晰(只要函数的类型定义明了,别人用你的函数就不会摸不着头脑)

看完这篇还敢说不会TypeScript吗?

2.安装TypeScript

新建一个文件夹,如 TypeScript-Study ,输入如下命令行,在全局环境下安装 tsc 命令

npm install -g typescript

安装完成之后,我们就可以在任何地方执行 tsc 命令了。

3.编写一个 ts 文件

新建一个简单的文件 hello.ts ,复制如下代码到文件中,这里建议使用 VS Code 编译器,内置了 TypeScript 支持,而且本身也是用 TypeScript 编写的(毕竟微软的亲儿子)

let hello: string = 'hello typescript'
console.log(hello)

然后输入下面命令行执行

tsc hello.ts

这时会发现文件夹多出了一个编译好的 js 文件 hello.js

var hello = 'hello typescript';
console.log(hello);

看完这篇还敢说不会TypeScript吗?

这里可以发现,在 ts 文件中使用 : 指定变量类型。

上面例子中我们给 hello 参数指定了 string 类型,编译成 JavaScript 时,检查的代码并不会被插入到 js 文件中。

这是因为 TypeScript 只会进行静态检查,如果发现有错误,编译的时候就会报错。

下面修改一下代码,把定义了 string 类型的参数改为 number 类型的参数

let hello: string = 'hello typescript'
hello = 2
console.log(hello)

编辑器中会提示错误,编译的时候也会出错

看完这篇还敢说不会TypeScript吗?

但仍然不妨碍我们执行生成 js 文件

var hello = 'hello typescript';
hello = 2;
console.log(hello);

如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json 中配置 noEmitOnError 即可,这里就不展开说明。

看完这篇还敢说不会TypeScript吗?

4.数据类型

JavaScript 的类型分为两种:原始数据类型和对象类型。

4.1原始数据类型

原始数据类型包括:布尔值、数值、字符串、nullundefined 以及 Symbol

布尔值

TypeScript 中,使用 boolean 定义布尔值类型

let isBoolean: boolean = true;

注意:要注意 boolean 和 Boolean 的大小写区别, boolean 是布尔值类型,而 Boolean 是构造函数。这里除了 null 和 undefined 之外,别的基本类型都一样。

数值

使用 number 定义数值类型

// 基本用法
let decLiteral: number = 1234567;
let hexLiteral: number = 0xf0ac;
let notANumber: number = NaN;
let infinity: number = Infinity;

编译结果:

// 基本用法
var decLiteral = 1234567;
var hexLiteral = 0xf0ac;
var notANumber = NaN;
var infinity = Infinity;

字符串

使用 string 定义字符串类型

let name: string = 'sam';

Undefined 和 Null

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

let u: undefined = undefined;
let n: null = null;

4.2对象类型

TypeScript 中,我们使用 interfaces 来定义对象类型。

确定属性

咱们直接举例说明吧

interface IPerson {
    name: string;
    age: number;
}

let sam: IPerson = {
    name: 'sam',
    age: 20
};

上面的例子中,我们定义了一个接口 IPerson ,然后定义了一个变量 sam ,变量的类型是 IPerson,这样就约束了 sam 的形状必须和接口 IPerson 一致。

注意:为了良好的编写习惯,建议接口的名称加上 I 前缀。

定义的变量 sam 比接口多或者少属性都是会报错的

interface IPerson {
    name: string;
    age: number;
}

// 错误(比接口少了个age属性)
let sam1: IPerson = {
    name: 'sam'
};

// 错误(比接口多了个gender属性)
let sam2: IPerson = {
    name: 'sam',
    age: 20,
    gender: 'man'
};

// 正确
let sam3: IPerson = {
    name: 'sam',
    age: 20
};

那么怎么解决这个问题呢,这里有个可选属性

可选属性

在接口上某个属性加个 ? ,表明不需要强制匹配该属性

interface IPerson {
    name: string;
    age?: number;
}

// 正确
let sam1: IPerson = {
    name: 'sam'
};

// 错误(此时我们还是不能在接口上添加未定义的属性)
let sam2: IPerson = {
    name: 'sam',
    gender: 'man'
};

看完这篇还敢说不会TypeScript吗?

为了解决添加未定义的属性,我们可以使用任意属性

任意属性

在需要添加任意属性的接口使用 [propName: string]一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

interface IPerson {
    name: string;
    // 此时的age类型为number,不是string类型的子集
    age?: number;
    // 确定属性和可选属性的类型都必须是string类型的子集
    [propName: string]: string;
}

// 错误(此时的age类型为number,不是string类型的子集)
let sam1: IPerson = {
    name: 'sam',
    age: 20,
    gender: 'man'
};

那么怎么解决这个问题呢,一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型

interface IPerson {
    name: string;
    age?: number;
    // 此时确定属性和可选属性的类型都必须是string或者number类型的子集
    [propName: string]: string | number;
}

// 正确
let sam1: IPerson = {
    name: 'sam',
    age: 20,
    gender: 'man'
};

// 正确
let sam2: IPerson = {
    name: 'sam',
    gender: 'man'
};

// 正确
let sam3: IPerson = {
    name: 'sam'
};

// 错误,任意属性未添加boolean类型
let sam4: IPerson = {
    name: 'sam',
    rich: false
};

只读属性

对象中的一些字段只能在创建的时候被赋值,后续无法更改

interface IPerson {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let sam: IPerson = {
    id: 123,
    name: 'Tom',
    gender: 'male'
};

// 错误,此时不能再次修改id的值
sam.id = 9527;

// Cannot assign to 'id' because it is a read-only property.

到这,我想你大概就了解了对象的类型(接口)了

看完这篇还敢说不会TypeScript吗?

5.任意值类型(any)

还记得上面的例子中,我们在给 hello 赋值的时候编译器会报错

let hello: string = 'hello typescript'
hello = 2
console.log(hello)

// Type '2' is not assignable to type 'string'.ts(2322)

如果把 hello 改为 any 类型,则表明 hello 可以赋值为任意类型。

let hello: any = 'hello typescript'
hello = 2
console.log(hello) // 2

当然这里不是希望你所有的类型都用 any ,这还写个🔨的 TypeScript ,这里一般建议引入第三方库或者实在无法确认在处理什么类型时的时候才使用。

看完这篇还敢说不会TypeScript吗?

6.类型推论

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

下面代码 hello 没有指定类型,但在编译器和编译时也会报错误提醒

let hello = 'hello typescript'
hello = 2

// Type '2' is not assignable to type 'string'.ts(2322)

因为它等价于

let hello: string = 'hello typescript'
hello = 2

但是如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型

let hello
hello = 'string2'
hello = 2

等价于

let hello: any
hello = 'string2'
hello = 2

7.联合类型

有时候我们希望声明一个变量时候包含多个类型,那么我们可以使用 | 分隔每个类型。还记得上面的例子吗

let hello: string = 'hello typescript'
hello = 2
console.log(hello)

此时我们修改一下 hello 参数

let hello: string | number = 'hello typescript'
hello = 2
console.log(hello)

这样就表明 hello 参数接受 stringnumber 类型

8.数组的类型

TypeScript 中,数组类型有多个定义的方式。由于本文是基础篇,笔者只讲比较常用的几种方法方便大家理解,后续进阶的用法笔者会另开文章说明。

直接定义

let arr1: number[] = [1, 2, 3, 4, 5];
let arr2: string[] = ["one", "two", "third", "four", "five"];
let arr3: any[] = [1, "two", false, "four", 5];

但这里要注意,定义后的数组 arr 的项中不允许出现非其他的类型( any 除外)

// 错误(two为string类型)
let arr1: number[] = [1, 'two', 3, 4, 5];

let arr2: number[] = [1, 2, 3, 4, 5];
// 错误(定义后的arr,push一个string类型是错误的)
arr2.push("six")

数组泛型

用数组泛型(Array Generic) Array<elemType> 来表示数组

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性,这里不做过多描述。

let arr1: Array<number> = [1, 2, 3, 4, 5];
let arr2: Array<string> = ["one", "two", "third", "four", "five"];
let arr3: Array<string | number | boolean> = [1, "two", false, "four", 5];
let arr4: Array<any> = [1, "two", false, "four", 5];

9.函数的类型

我们先来复习一下 JavaScript 里面声明常见的定义函数的方式(函数声明和函数表达式)

(感谢@cnsdwu错误提示,已修改)

// 函数声明
function sum1(x, y) {
    return x + y;
}

// 函数表达式
let sum2 = function (x, y) {
    return 
}

9.1 函数声明

TypeScript 声明函数时,我们需要把函数的输入和输出都要考虑在内

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

// 用法
// 错误
sum(1);
sum(1, 2, 3);

// 正确
sum(1, 2);

可见,定义好的函数,如果输入了多的或者少的参数都是不被 TypeScript 允许的。

9.2函数表达式

你可能会这样写一个函数表达式

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

这样只对右边的匿名函数进行了类型定义,而左边的参数 sum2 是通过类型推断出来的,并不算一个完整的 TypeScript 表达式。正确写法如下

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

注意:在 TypeScript 的类型定义中的 => 用来表示函数的定义,箭头左边是输入类型,箭头右边是输出类型,不要和 ES6 的箭头函数混淆。

9.3可选参数

和接口的可选属性相似,我们也可以用 ? 来给函数表示可选的参数

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

// 用法
sum3(1,2)
sum3(1)

注意:可选参数的后面不允许放必须参数,因为这样调用的时候无法识别。

除非添加参数默认值,则可无视这个限制

// 错误写法(可选参数y后面跟着必须参数z)
function sum3 (x:number, y?:number, z: number) {
    return x + y + z
}

// 给可选参数y添加参数默认值
function sum4 (x:number, y:number = 1, z: number) {
    return x + y + z
}

10.类型断言

用来手动指定一个值的类型叫做类型断言。

语法

有两种写法,尖括号 <类型>as

let str: string = "this is a string";

// 尖括号<>
let strLength1: number = (<string>str).length;
// as
let strLength2: number = (str as string).length;

这里建议大家统一使用 as 语法,因为在 React 的 tsx 语法中必须使用 as。

TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法,如

function getLength(str: string | number): number {
    // 错误,因为 number 类型没有 length 方法
    return str.length;
}

// Property 'length' does not exist on type 'number'.ts(2339)

为此,我们需要用到类型断言

function getLength(str: string | number): number {
    if (typeof str === 'number') {
        // 此时将 str 的类型判断为 number 类型,可以使用 number 类型的属性和方法
        return (str as number).toString().length
    } else if (typeof str === 'string') {
        // 此时将 str 的类型判断为 string 类型,可以使用 string 类型的属性和方法
        return (str as string).length
    } else {
        throw 'error'
    }
}

恭喜你👏看到这里,TypeScript 最为需要掌握的基础你已经学完了,不过重头戏才刚刚开始😅

看完这篇还敢说不会TypeScript吗?

11.枚举(Enum)

枚举是 TypeScript 中,对 JavaScript 标准数据类型的补充,例如一个 Http 包含哪些状态,一星期是从星期一到星期日等,我们都可以用枚举表达,如果你用过后端的编程语言,应该对枚举不陌生。

不同于对象定义的 key-value 中,只能通过 key 去访问 value 的值,在 enum 中,既可以通过 key 访问 value 的值,也可以通过 value 访问 key 的值。

枚举用 enum 关键字来定义

简单定义

enum Person {
    Male,
    Female
}

上面的代码编译如下

var Person;
(function (Person) {
    Person[Person["Male"] = 0] = "Male";
    Person[Person["Female"] = 1] = "Female";
})(Person || (Person = {}));

可见,若没给枚举的成员赋值,那么会默认从 0 开始递增。

当然我们也可以给枚举手动赋值

手动赋值

enum Person {
    Male = 7,
    Female      // 此时 Female 会为 8
}
// 因为未赋值的枚举会接着上一个枚举项递增,因此此时 Female 会为 8

上面提到的都是常数项,其实枚举还有一个类型叫 计算所得项

enum Person {
    Male,                    // 常数项
    Female = "female".length // 计算所得项
}

// 如果计算所得项后面是没有赋值的项,则会报错
// 错误
enum Person {
    Male = "man".length,
    Female
}

// 正确
enum Person {
    Male = "man".length,
    Female = 8
}

常数枚举

常数枚举是用 const enum 定义的,常数枚举在编译的阶段会被删除,既在编译后的文件不存在编译后的常数枚举,且不能包含计算成员

// 正确
const enum Person {
    Male,
    Female
}

// 错误
const enum Person2 {
    Male,
    Female = "female".length
}

let person = [Person.Male, Person.Female]

编译结果

var person = [0 /* Male */, 1 /* Female */];

外部枚举

外部枚举是用 declare enum 定义的,外部枚举用来描述已经存在的枚举类型的形状。

declare enum Person {
    Male,
    Female
}

let person = [Person.Male, Person.Female]

编译结果

var person = [Person.Male, Person.Female];

12.元组(Tuple)

元组合并了不同类型的对象,需要以元组所定义的顺序预定数据类型。

下面举个简单的例子

let tuple1: [string, number];

// 正确
tuple1 = ["one", 2];

// 错误
tuple1 = ["one", "two"];
tuple1 = [1, 2];
tuple1 = ["one", 2, 3]; // 未在元组中定义
tuple1 = [true, 2]      // 元组在index为0中只接受string类型

13.泛型(Generics)

还记得上文数组泛型的定义吗

let arr1: Array<number> = [1, 2, 3, 4, 5];

这里的 Array<number> 只允许数组的每一项都要为 number 类型,但有的时候,我们希望返回值的类型与传入参数的类型是相同的,因此有了泛型。

这里是官方文档的例子:

function identity<T>(arg: T): T {
    return arg;
}

这里的 identity 函数就被称为泛型,因为它可以用于多个类型,我们给 identity 添加了类型变量 TT 会帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T 当做返回值类型。

定义了泛型之后,我们可以这样使用

let output1 = identity<string>("myString");

// 类型推论。编译器会根据传入的参数自动地帮助我们确定 T 的类型

// 此时的 T 为 string 类型
let output2 = identity("myString");
// 此时的 T 为 number 类型
let out2 = identity(123)
// 此时的 T 为 boolean 类型
let out3 = identity(true)

泛型约束

如果我们想打印 arg 的长度,会发现编译器报错。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

这是因为泛型 T 是默认把 arg 参数当作是任意或者所有类型,而 number 类型没有 length 属性,所以会报错。

如果我们传入数字数组,将返回一个数字数组,因为此时 T 的的类型为 number。 这可以让我们把泛型变量 T 当做类型的一部分使用,而不是整个类型,增加了灵活性。

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // arg
    return arg;
}

泛型接口

我们把上面例子的对象字面量换位接口

interface Iidentity {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: Iidentity = identity;

泛型类

与泛型接口类似,泛型也可以用于类的类型定义中

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

14.参考文章

15.结尾

看到这里,你还敢说不了解 TypeScript 吗?如果还不了解,那一定是你还没给这篇文章点赞👍👍👍!!!

看完这篇还敢说不会TypeScript吗?

看得不过瘾?这里还有别的文章