原始类型

接下来我们先来了解一下,目前我们JavaScript当中6种原始数据类型,在TypeScript当中的一个基本应用。

那这里绝对多数的情况跟我们在Flow当中了解到的都是类似的,我们先将配置文件当中的target修改为常用的es5,因为这样的话我们才能发现一部分的特殊情况。这里我们快速过一下这几个原始数据类型。

那首先是string类型,那他是只能够存放字符串的。

const a: string = 'footer';

然后是number类型,那他的值就只能够存放数字, 当然这个number也包括NaN和Infinity这两个特殊值。

const b: number = 100;

那再然后就是boolean类型,那这个类型呢他就只能够存放true或者是false

const c: boolean = true; // false

那这三种原始值类型相比较Flow当中有所不同的是,TypeScript当中这三种类型默认是允许为空的,也就是这里我们可以为他们赋值为null,或者是undefined。例如我们这里定一个字符串类型的变量吗,我们让他等于null。

const d: string = null;

但是我们通过试验发现这里的结果却是错误的,那这就要说到这个严格模式和默认模式之间的一个差异了,我们将tsconfig.json中的strict设置为false,也就是关闭严格模式。这个时候我们回到代码当中就不会报错了。

那这是在非严格模式下,我们的string,number和boolean他们都可以为空,那严格模式呢他是不允许的。

当然我们这里的strict他是开启了所有的严格检查的选项,那如果说我们只是要去检查我们的变量不能为空的话我们可以使用strictNullChecks这样一个单独的选项,那这样一个选项他就是完全用来检查我们变量不能为空的。

那再者呢就是一个void类型,也就是空值,那这种类型呢一般我们会在函数没有返回值的时候我们去标记这个函数没有返回值类型,那他只能存放null或者是undefined,那在严格模式下他只能是undefined。

const e: void = undefined;

然后就是null类型和undefined类型,那这两个类型本身并没有什么特殊的情况,这里我们不用管。

const f: null = null;

const g: undefined = undefined;

那最后就有一个需要注意的东西了,那这个地方我们再来尝试一下ECMAScript2015当中新增的symbol类型,那这个类型他同样只能够用于存放symbol类型的值,当时呢,让我们直接去使用这样一个类型去创建symbol的值过后呢我们会发现这里的代码报出了一个错误。

const h: symbol = Symbol();

那为什么这个地方会报出错误呢,我们下面介绍。

标准库声明

刚刚我们在尝试使用全局的symbol函数去创建一个symbol的值的时候报出了错误,那想要解决这个问题很简单,我们只需要按照错误描述中的一个提示,直接将编译选项的target修改为es2015就好了。

但是这到底是为什么呢,那这里我们仍旧将target设置为es5,我们继续去探索一下这样的问题,其实这个原因很简单。

我们应该知道symbol他实际上就是一个JavaScript当中内置的一个标准对象,就跟我们之前所使用的的Array,Object这些性质是相同的,只不过symbol他是ES6当中新增的,对于这种内置的对象,其实他自身也是有类型的。

而且这些内置对象的类型都在TypeScript当中已经帮我们定义好了,那这里我们可以先去使用一下Array,然后我们在这个Array上面通过右键找到Go to Definition, 也就是转到定义。

那这样的话我们就可以找到这个类型的声明文件,在这个文件中声明了所有内置对象对应的类型。

那按照道理来说symbol他也应该在这个文件当中有对应的类型声明,但其实细心一点你就已经发现这里的这个声明文件它叫做lib.es5.d.ts。那很明显我们这个文件它实际上是ES5标准库所对应的一个声明文件。

而我们的symbol他是ECMAScript2015当中所定义的,所以自然不会在这个文件当中去定义他对应的一些类型。

那我们可以再回到TypeScript的配置文件当中,那这里我们设置的target是es5,那对于ES5这种情况下,那我们对标准库的引用默认只会引用ES5所对应的标准库,所以我们的代码当中直接去使用ES6的symbol就会报错。

其实呢这里不仅仅是symbol,任何一个在ES6中新增的对象我们直接去使用都会遇到这样一个问题。

要解决这个问题的办法有两种,第一种是直接修改我们target为es2015,这样的话我们默认的准标准库就会引用ES6所对应的标准库。

如果说我们必须要编译到es5的话,那我们也可以使用lib选项去指定我们所引用的标准库,我们解除lib选项的注释,然后我们在这个数组当中添加一个ES2015, 那这样也是可以的。

"lib": ["ES2015"]

不过这个时候我们在代码中添加一句console.log会发现,console会报错,那出现这个问题的原因实际上跟我们刚刚所分析的是一样的。

console对象在浏览器环境中是BOM对象提供的,而我们刚刚所设置的lib当中只设置了一个ES2015, 所以说所有的默认的标准库都被覆盖掉了,那这里我们需要把这些默认的标准库再把它添加回来。

不过我们需要注意一点的是TypeScript当中把BOM和DOM都归结到一个标准库文件中了,就叫做DOM,也就是我们这里只需要添加一个DOM的引用就可以了。完成以后console就不再报错了。

那我们也可以在这个console上右键转到Go to Definition,那这样我们就可以看到刚刚所添加进来的这个DOM的标准库文件。

那以上我们通过解决代码当中找不到symbol这样一个问题,介绍了一下在TypeScript中标准库的一个概念。

那所谓标准库,实际上就是内置对象所对应的声明文件,那我们在代码当中使用内置对象就必须要引用对应的标准库,否则TypeScript就找不到所对应的类型,那他就会报错。

中文错误消息

这里我们再来介绍一个对使用中文的开发者非常有帮助的一个小技巧,就是让TypeScript去显示中文的错误消息。

那TypeScript它本身是支持多语言化的一个错误消息的,默认他会根据操作系统对开发工具的语言设置选择一个错误消息的语言。

但是我们这里所使用的是英文版的vscode,所以我们看到的错误消息都是英文的。

那如果我们想强制他显示中文消息,那我们可以在使用tsc命令的时候加上一个--local的这样一个参数,然后我们后面跟上一个zh-CN, 也就是说我们使用中文的错误消息。

tsc --local zh-CN

那这样的话对于绝大多数的错误都会以中文的形式去显示错误消息,那对于vscode当中的错误消息我们可以在配置选中当中去配置。

那我们打开配置选项,然后我们搜索一下typescript local 我们找到这样一个选项,把他设置为zh-CN。

那此时呢,我们的vscode当中所报出来的错误提示也就是中文的了。

不过这里我们并不推荐大家这么做,因为在很多时候我们对于一些不理解的错误我们会使用google这样的搜索引擎去所有一些相关的资料,那如果说我们看到的是中文的错误提示,那根据这种中文的错误消息呢我们是很难搜索到有用的东西的。

所以说一般对于开发相关的地方呢,我们建议更多的还是使用英文的方式。

作用域问题

那我们在学习TypeScript的过程当中,肯定要涉及到在不同的文件当中去尝试TypeScript的一些不同的特性,那这种情况下我们就可以能遇到不同文件当中会有相同变量名称的这样一个情况。

例如我们在两个不同的文件当中定义相同的变量名,那此时呢我们这个TypeScript就会报出一个重复定义变量的这样一个错误。

那这就是因为我们这个a变量目前他是定义在全局作用域上面的,所以TypeScript在去编译整个项目的时候就会出现这样一个错误,那解决这个问题的办法呢自然是把他们分别装到不同的或者单独的作用域当中。

例如我们可以用一个立即执行函数创建一个单独的作用域,那这个时候我们把变量放到这个函数的内部,那这样就不会有错误了。

(function() {
    const a = 123;
})()

或者我们也可以在这个文件中使用export去导出一下,也就是使用一下ES Module, 那这样的话我们这个文件他就会作为一个模块。

export const a = 123;

那模块是有单独的模块作用域的, 那这里我们可以在文件的最后去添加一个export {},那需要注意这里的{}只是export的一个语法,他并不是导出了一个空对象,那这样的话我们在这个文件当中,所有的成员,他就变成了我们这个模块作用域中的局部成员了,也就不会再出现冲突的问题了。

那这样一个问题呢,实际上我们在实际开发时呢一般不会用到,因为在绝大多数情况下我们每个文件都会以模块的形式去工作,只不过我们后面的每个案例它里面难免会用到一些重复的变量,所以说我们每个案例中我们都会添加一个export。

Object类型

TypeScript中的Object他并不单指普通的对象类型,而是泛指所有的非原始类型,也就是对象,数组还有函数。

例如我们这里再来定义一个叫做foo的变量,我们把它的类型标记为object,那注意这里的object是全小写的。那这样一个变量他就能接收对象,或者是数组或者是函数都是可以的。

const foo: object = function() {}

那如果我们去接收其他任何一种原始值,那他会发生错误,那这里我们尤其需要注意的就是object类型他并不单指普通的对象。

那如果我们需要普通的一个对象类型, 我们需要使用类似对象字面量的语法去标记我们这里的类型, 例如我们这里去限制我们的对象必须有一个叫做foo的number类型的属性,我们就用foo: number。

const obj: { foo: number } = { foo: 123 }

如果我们需要去限制多个成员,这里我们仍然可以用逗号的方式在后面继续去添加。

那这里的对象类型限制呢,他的要求是赋值的对象结构他必须要跟我们这里类型的结构完全一致,不能够多,也不能够少,也就是说如果我们在对象当中去添加了一些额外的成员,那这里语法上就会报出类型错误。

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

那当然,在TypeScript当中去限制对象类型,我们应该使用更为专业的一个叫做接口的东西,那关于接口呢,我们会单独介绍。这里我们只需要了解两个点。

第一呢就是object类型他并不单指对象,而是除了原始类型以外的其他类型,第二点就是对象的类型限制我们可以使用这种类似对象字面量的语法的方式,但是呢,我们说了,更专业的方式是使用接口,而接口在后面我们会有详细的介绍。

数组类型

TypeScript当中定义数组的方式跟Flow当中几乎完全一致,那他也有两种方式,第一种就是使用Array泛型,例如我们这里定义一个arr,那他的类型就是Array,这里的元素类型我们可以设置为number,这就表示一个纯数字组成的数组。

const arr: Array<number> = [1];

那第二种方式要更为常见一些,就是使用元素类型然后加上[]这样一种形式。

const arr: number[] = [1];

元组类型是一种特殊的数据结构,其实元组就是一个明确元素数量以及每一个元素类型的数组,那各个元素的类型他不必要完全相同。

那在TypeScript当中我们可以使用类似数组字面量的这种语法去定义元组类型,例如我们这里先去定义一个tuple变量,那他的类型是一个[], 然后第一个元素是number,第二个元素是string,那这个时候我们这个tuple变量他就只能够存放两个对应类型的元素了。

const tuple: [number, string] = [10, 'yd'];

那如果我们想要访问我们元组当中的某一个元素,我们仍然可以使用数组的方式去访问。

那这种元组呢,他一般可以用来在一个函数当中去返回多个返回值,那这种类型呢在现在实际上是越来越常见了,比如我们在react当中最新添加的useStatus这样的hook函数当中我们返回的就是一个元组类型。

再比如我们使用ES2017当中所提供的Object.entries这样一个方法获取对象当中所有键值数组,那这里我们所得到的的每一个键值他就是一个元组,因为他是一个固定长度的。

枚举类型

我们在应用开发过程中,经常会涉及到需要用某几个数值去代表某种状态,例如我们这里有一个文章对象,这个对象当中有一个文章的状态,我们可以使用数字去表示不同的状态,比如0代表草稿,1代表未发布,2代表已发布。

const post = {
    title: 'Hello TypeScript',
    status: 1,
}

那也就是说我们这个状态的取值他只可能是0,1,2这三个值,那如果我们直接在代码当中去使用0,1,2这样的字面量去表示状态的话,那时间久了我们就可能搞不清楚这里写的数字他到底对应的是哪个状态。

而且时间长了还可能混进来一些其他的值,例如我们去使用0,1,2以外的一个数字。

那这种情况下我们使用枚举类型是最合适的,因为枚举类型有两个特点,第一点就是他可以给一组数值分别取上一个更好理解的名字,第二点就是一个枚举中,只会存在几个固定的值。那并不会出现超出范围的可能性。

在很多传统的编程语言当中,枚举是一个非常常见的语言结构,不过在js当中并没有这样的数据结构。

很多时候我们都是使用一个对象去模拟实现枚举。

例如我们这里定义一个posTypeScripttatus对象,这个对象中有三个成员,分别是draft:0, unpublished: 1, published: 2

const posTypeScripttatus = {
    draft: 0,
    unpublished: 1,
    published: 2
}

然后我们就可以在代码中使用对象中的一些属性去表示文章的状态,这样的话我们在后面使用的过程中就不会出现我们刚刚所说的那样的问题。

在TypeScript当中他有一个专门的枚举类型,我们可以使用一个enum关键词去声明枚举。

enum posTypeScripttatus = {
    draft = 0,
    unpublished = 1,
    published = 2
}

需要注意的是这里我们使用的是=而不是对象当中的:,我们使用枚举他的使用方式跟对象是样的,同样也是打点调用。

我们这里枚举类型的值可以不用=号的方式去指定,如果我们不指定,默认的话这个值会从0开始累加,如果我们给枚举当中第一个成员去指定了值,那后面这些值都会在这个基础之上去累加。

枚举的值除了可以是数字以外还可以是字符串,也就是字符串枚举,我们可以把每个值的初始化为字符串,由于字符串是无法像数字一样自增长的,所以说是字符串枚举的话,就需要手动去给每个成员初始化一个明确的字符串的值。

那这种字符串枚举他其实并不常见,所以这里我们就不做过多的介绍了。那以上就是枚举类型的基本语法以及使用。

关于枚举,这里我们还需要了解一个内容就是,枚举类型他会入侵到运行时的代码,这么说可能不好理解,通俗一点来说就是他会影响代码编译后的结果。

我们在TypeScript当中使用的大多数类型他在经过编译转换过后最终都会被移除掉,因为他只是为了我们再编译过程中可以做类型检查,而枚举呢,他不会。

他最终会编译为一个双向的键值对对象,这里我们可以编译看一下。打开成成的js文件我们就可以看到。双向的键值对对象。

var posTypeScripttatus;
(funcion (posTypeScripttatus) {
    posTypeScripttatus[posTypeScripttatus['draft'] = 0] = 'draft';
    posTypeScripttatus[posTypeScripttatus['unpublished'] = 1] = 'unpublished';
    posTypeScripttatus[posTypeScripttatus['published'] = 2] = 'published';
})(posTypeScripttatus || (posTypeScripttatus = {}))

那所谓的这个双向的键值对,其实就是可以通过键去获取值,可以通过值再去获取键,那我们这里仔细观察这段代码你会发现,无外乎也就是把我们枚举的名称最为对象的键去存储了枚举的值。然后再用值作为键再去存一下我们枚举的键。

那这样做的目的呢就是为了可以让我们动态的根据枚举值,也就是根据0,1,2这样的值去获取枚举的名称。

也就是说我们可以在代码当中通过所引起的方式去访问对应的枚举名称。

posTypeScripttatus[0]; // draft

那如果说我们确认我们代码当中不会使用所引起的方式去访问枚举,那我们就建议大家去使用常量枚举。

那常量枚举的用法呢就是在枚举的enum这个关键词前面去加上一个const。

const enum posTypeScripttatus = {
    draft,
    unpublished,
    published
}

编译过后我们再次找到生成的js,这离我们就可以发现,我们所使用的枚举会被移除掉,而我们使用枚举值的地方都会被替换为实际的值,枚举的名称会以注释的方式去标注。

以上就是在TypeScript当中枚举类型的一些主要特征。

函数类型

接下来我们来了解一下TypeScript当中函数的类型约束,那函数的类型约束无外乎就是对函数的输入输出进行类型限制。

那输入就是我们的参数输出指的就是返回值,不过呢,在JavaScript当中有两种函数定义的方式,分别是函数声明和函数表达式。

所以我们这需要分别去了解一下这两种方式下,函数如何进行具体的类型约束。

这里我们来声明一个函数,也就是使用函数声明的方式去定义一个函数,这个函数接收两个参数,在里面我们去返回一个字符串。

function func1 (a, b) {
    return 'func1';
}

那函数声明的这种方式他的类型约束就比较简单,直接在函数每一个参数后面添加对应的类型注解, 然后函数返回值的类型注解呢需要添加在括号后面。

那添加完对应的类型注解过后呢,我们再调用这个函数时只能够传入相同类型的参数,而且这里的参数的个数也必须要完全相同。

那如果我们需要某一个参数是可选的,我们就可以使用可选参数这样一个特性, 也就是在参数的名称后面添加一个?, 那这样的话这个参数就变成了一个可选参数, 我们在调用时可以传也可以不传。

function func1 (a: number, b: number, c?: number): string {
    return 'func1';
}

func1(100, 200);
func1(100, 200, 300);

或者我们也可以使用ES6新增的这种参数默认值的特性,因为添加了参数默认值过后,这个参数本身就是可有可无的了。

function func1 (a: number, b: number, c?: number = 10): string {
    return 'func1';
}

func1(100, 200);

当然对于这里我们所使用的可选参数也好,默认参数也好他都必须要出现在我们参数列表的最后。这一点的原因其实很简单,因为参数会按照位置进行传递,如果说可选参数出现在了必选参数的前面,那这个时候必选参数是没有办法拿到正常对应的值。

如果我们需要接收任意个数的参数,我们可以使用ES6的reset操作符。

function func1(...reset: number[]): string {
    return 'func1';
}

那以上就是函数声明的一个对应的类型限制,那我们可以再来看一下函数表达式所对应的类型限制。

那这里我们再使用函数表达式的方式去定义一个函数。那函数表达式他也可以使用相同的方式去限制函数的参数和返回值的类型。

const func2 = function(a: number, b: number): string {
    return 'func2';
}

不过我们这里这个函数表达式,他最终是放到一个变量中的。那接收这个函数的变量,他也应该是有类型的。

那一般TypeScript它都能根据我们这个函数表达式推断出来我们这个变量的类型,不过呢,如果我们是把一个函数作为参数传递,也就是回调函数的这种方式,那对于回调函数这种情况下,我们就必须要去约束我们这个回调函数这个参数,也就是这个形参的类型。

那这种情况下我们就可以使用这种类似箭头函数的方式去表示我们这个参数,他可以接收什么样的一个函数。

const func2: (a: number, b: number) => string = function(a: number, b: number): string {
    return 'func2';
}

那这种方式我们在以后去定义接口的时候会经常用到,那具体的我们到时候再说。

任意类型

由于JavaScript自身是弱类型的关系,很多内置的API它本身就支持接收任意类型的参数。

而TypeScript又是基于JavaScript基础之上的,所以说我们难免会在代码当中需要去用一个变量去接收任意类型的数据。

例如我们这里定义一个叫做stringify的函数,这个函数接收一个value参数,在内部我们使用JavaScriptON.stringify方法将value序列化成JavaScripton字符串,那这里的这个value参数他就应该支持接收任意类型的参数。

function stringify (value) {
    return JavaScriptON.stringify(value);
}

因为本身这个JavaScriptON.stringify就可以接收任意类型的参数,那此时我们就必须要有一个类型可以去用来接收任意类型参数。

function stringify (value: any) {
    return JavaScriptON.stringify(value);
}

那any就是这样一个可以用来接收任意类型数据的一种类型,需要注意的是any类型仍然属于动态类型。他的特点跟普通的JavaScript变量是一样的。也就是可以用来接收任意类型的值。而且在运行过程中它还可以用来接收其他类型的值。

那正式因为他有可能存放任意类型的值,所以说TypeScript他不会对any这种类型做类型检查,那这也就意味着我们仍然可以像之前在JavaScript中一样在他上面去调用任意的成员。语法上都不会报错。

那也正式因为any类型他不会有任何的类型检查,所以说他仍然会存在类型安全的问题,我们轻易不要使用这种类型。

但是有的时候我们在去兼容一些老的代码的时候呢我们可能难免会用到这样一个any的类型。

隐式类型推断

在TypeScript当中如果我们没有明确通过类型注解去标记一个变量的类型,那TypeScript他会根据这个变量的使用情况去推断这个变量的类型。那这样一种特性叫做隐式类型推断。

例如我们这里使用let关键词定义一个叫做age的变量,这个变量我们赋值了一个数字,这样一个变量的类型就会被TypeScript推断为number类型。

let age = 18;

如果说我们这个地方再去给这个变量重新赋值一个字符串,此时语法上就会出现类型错误, 因为这个时候age他已经被推断为了number类型,也就说说这种用法实际上就相当于给age变量添加了: number的类型注解。

let age = 18;

age = 'string';

那如果说TypeScript他无法去推断一个变量具体的类型,那这个时候他就会将这个类型标记为any。

例如我们这定义一个叫做foo的变量,当我们去声明这个变量的时候我们并没有为他赋值,那这个时候这个foo就是any类型, 也就是一个动态类型,那也就是说我们可以在后续代码中去往这个变量当中去放入任意类型的值。语法上都不会报错, 那这就是隐式类型推断。

let foo;

foo = 'string';

虽然说我们在TypeScript当中支持隐式类型推断,而且这种隐式类型推断可以帮我们简化一部分代码,但是我们仍然建议尽可能给每一个变量去添加明确的类型,因为这样的话会便于我们后期去更直观的理解我们的代码。

类型断言

在有些特殊的情况下TypeScript无法去推断出一个变量的具体类型,而我们作为开发者,我们根据代码的使用情况我们是可以明确知道这个变量到底是什么类型的。

例如我们这里有一个数组,我们假定这个数组他是从一个可以明确的接口当中获取到的,那此时我们调用这个数组对象的find方法从这个数组当中去找出第一个大于0的数字。

const nums = [110, 120, 119, 112];

const res = nums.find(i => i > 0);

那很明显,这里他的返回值一定是一个数字,因为我们这个数组当中一定会有一个大于0的数字。

但是对于TypeScript来讲,他并不知道,他所推断出来我们这个地方的返回值呢是一个number或者undefined,因为他会认为我们有可能找不到。

那此时我们就不能把这个返回值当做一个数字去使用,那这种情况下我们就可以去断言res是一个number类型的。

const nums = [110, 120, 119, 112];

const res = nums.find(i => i > 0);

const square = res * res;

那断言的意思实际上就是明确告诉TypeScript, 你相信我,我们这个地方res一定是number类型。

那类型断言的方式有两种,第一种是使用as关键词,那我们这里再去定义一个num1的变量,让他等于res as number。那此时我们这个编译器就能明确知道我们这个num1他是一个数字了。因为我们是明确告诉TypeScript我们这里是一个number的。

const num1 = res as number;

那另一种语法呢就是在这个变量的前面去使用<>的方式去断言,我们这里定义一个num2,我们让他等于res。那这个效果仍然是一样的。

const num2 = <number>res;

但是这种<>的方式,他有一个小问题,就是当我们在代码当中使用了jsx的时候,例如我们在写react代码的时候,难免会用到jsx,那这里的他会跟jsx中的标签产生语法上的冲突,所以那种情况下就不能使用这样的方式了。

所以这里我们推荐使用第一种as方式,那以上就是类型断言的基本用法,那这种用法很多时候我们都可以用来去辅助TypeScript去更加明确我们代码当中每一个成员的类型。

那这里还需要注意一点的是,类型断言他并不是类型转换,也就是说这里他并不是把一个类型转换成了另外一个类型。

因为类型转换他是代码在运行时的一个概念,而我们这个地方类型的断言呢他只是在编译过程中的一个概念,当代码编译过后呢,这个断言他也就不会存在了,所以说他跟我们类型转换是有本质上的差异的。


欢迎关注,更多系列文章