循序渐进理解TypeScript类型模式
栏目: JavaScript · 发布时间: 5年前
内容简介:TypeScript是微软推出的一个强类型JavaScript超集,具备了比JavaScript更严禁的类型定义,备受程序员喜爱的编辑器VSCode就是使用TypeScript写的,后面简称ts。在使用js的时候,一个变量可以被任意赋值,而且不管你怎么写总是不会报错,这就意味着很多时候错误被埋藏在看似正常的代码中。不同于js的直接执行,ts代码必须通过编译成js来运行,很多错误会在编译的时候就报错,大大降低了发现低级错误的成本。尤其在写大型项目的时候,ts的优势更加明显。例如我们写了一个组件,组件使用了一个
TypeScript是微软推出的一个强类型JavaScript超集,具备了比JavaScript更严禁的类型定义,备受 程序员 喜爱的编辑器VSCode就是使用TypeScript写的,后面简称ts。
在使用js的时候,一个变量可以被任意赋值,而且不管你怎么写总是不会报错,这就意味着很多时候错误被埋藏在看似正常的代码中。不同于js的直接执行,ts代码必须通过编译成js来运行,很多错误会在编译的时候就报错,大大降低了发现低级错误的成本。
尤其在写大型项目的时候,ts的优势更加明显。例如我们写了一个组件,组件使用了一个传过来的对象作为props,突然我们需要给组件增加一个功能,需要多传入一个数据,因此我们需要给props对象添加一个属性。如果使用js,这种情况下确定哪些地方需要做对应的修改是一件很头疼的事情,而ts则很容易,我们修改一下props对象的接口类型,所有需要修改的地方就都会报错,依次修改报错的地方就可以了。
网上各类ts的教程很多,但是很少有站在一个不懂ts的立场上来写的,这篇文章的目的,就是带领那些熟悉js的人,一步步理解ts添加的类型模式,把一个陡峭的山坡,变成一级级的台阶,帮助想用ts的人快速入门。
基础类型
在js中,很多的东西都是以对象的形式存在的,除了最基础的5种数据类型:
- number
- string
- boolean
- null
- undefined
这五种最基本的数据类型的定义是很直白的,就是冒号加名字,如下所示:
let num: number = 0; let str: string = ""; let boo: boolean = false; let foo: null = null; let bar: undefined = undefined; 复制代码
从上面的例子可以看出,ts和js的区别,就是在变量后面紧跟了一个冒号,冒号后面写下了变量的类型。记住这一点,下面的内容就能理解了,如果还没有理解,就需要反复体会一下,理解了再继续往下。
void
除了上面的这些类型以外,ts还增加了一种特殊的类型: void
let a: void = null; 复制代码
一个void类型的变量只能被赋值为null或者undefined,它通常用在函数中,指定函数的输出为void意味着这个函数没有返回值。
any
除了上面介绍的6大基础类型之外,还一个万能类型: any
let a: any = "hello,world!" 复制代码
万能类型就是js本身的样子,一个变量可以被定义为任何类型,也是使用ts的时候应该尽量避免使用的类型,使用过多的any就让ts退化成了js,失去了使用ts的意义。
联合类型
ts支持联合类型,意味着一个变量可以有不止一个类型,如下所示:
let name: string | number = 'mike'; name = 18; // 允许赋值两种类型的数据 复制代码
联合类型赋予了ts变量一定的自由,但是还不如js那么自由。对联合类型的属性和方法的访问是受限的,在给name赋值之前只能访问公共的属性与方法,在赋值以后则只能访问由被赋值的类型拥有的属性与方法。
联合类型是一种处于基础类型与any类型之间的中间态。
对象的类型:interface
有了基础的类型,还需要一种方法来定义对象的类型,因为对象就是一些基础类型拼凑起来的,所以缺少的只是一个封装的方法,因此ts引入了interface关键字。
声明一个interface:
interface person { name: string; age: number; } 复制代码
然后就可以使用这个interface来限制一个对象的类型:
let tom: person = { name: 'tom', age: 12 } 复制代码
有了interface我们就可以把一堆基础类型封装成一个对象的类型。
需要注意的是声明接口的时候用的是 ;
分号,而对象中用的是 ,
逗号。
可选类型
在对象中除了必须的属性与方法以外,可能还有些属性或方法不是必须的,于是就诞生了可选类型。在interface中放一个可选类型的方式如下所示:
interface person { name: string; age?: number; // 可以没有这个属性 } 复制代码
这样我们在申明一个类型为person的对象时就可以不给它age属性, 让这个对象可以变小 。
任意类型
一个对象中除了必须的类型和可有可无的类型外,我们还希望能后期增加类型,于是就诞生了任意类型。给interface添加一个任意属性的方式如下:
interface person { name: string; [propName: string]: string; } 复制代码
这样我们就可以给一个person类型的对象添加值为string的属性, 让这个对象可以变大 。
这里需要注意一个很关键的问题:当接口中存在任意属性时,其他的所有属性都必须是任意属性的子集!,如下所示的接口定义就是错误的:
interface person { name: string; age?: number; [propName: string]: string; } 复制代码
上面的可选属性age的变量类型是number,不是任意属性string的子集,所以编译的时候就会报错。
只读类型
我们可以给interface声明一个只读属性或方法,只能在初始化变量的时候赋值而不允许后续的修改,只要在属性或方法前面加上一个 readonly
关键词,如下所示:
interface person { readonly id: number; name: string; } 复制代码
特殊对象的类型
有了一般对象的类型,还剩下一些特殊的对象的类型:数组与函数。
数组
数组的类型有三种定义方式。
数组声明
第一种是最简单也最直观的定义方式,直接在元素类型后面加上一对 []
,如下所示:
let arr: number[]; 复制代码
这就定义了 arr
为一个number数组。
接口定义
既然数组是一种特殊的对象,自然也就可以使用对象的类型定义方式:interface,具体如下所示:
interface NumberArray { [index: number]: number; } let arr: NumberArray; 复制代码
上面定义的NumberArray就是一种键为数字,值也为数字的对象,也就是数字数组。
泛型定义
除了上面两种定义方式,还可以使用泛型来定义一个数组的类型,这一点在后续泛型的章节中讨论。
元组
弱类型的js允许在一个数组中保存不同类型的数据,元组就是一种混杂类型的数组:
let mike: [string, number] = ["boy", 12]; 复制代码
函数
函数的声明方式有很多种,ES6引入的箭头函数得到了广泛的使用,在此之前函数的声明主要有两种方法:函数声明与函数表达式。这两种写法的类型定义方式是不一样的。
函数声明
函数声明使用function关键字来声明一个函数:
function sum(x, y) { return x + y; } 复制代码
函数声明的ts类型定义是比较简单的:
function sum(x: number, y: number): number { return x + y; } 复制代码
比较直观也很好理解,只需要记住函数的输出在形参的括号后面接冒号定义,别的都很直白。
函数表达式
函数表达式使用let或const(实际上几乎都是const)来声明一个匿名函数并赋值给一个变量:
const sum = function(x, y) { return x + y; } 复制代码
函数表达式的类型定义是极其复杂的,如下所示:
const sum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; }; 复制代码
不仅需要在匿名函数这里声明输入输出的变量类型,还需要声明保存这个函数的指针的类型,而且这个类型写起来极其复杂,使用了与箭头函数一样的 =>
操作符,这种写法让人讨厌的地方在于感觉需要重复写一遍类型定义。
接口定义
函数也是特殊对象,可以使用接口来定义输入输出类型,这样就意味着,我们会指定一个变量,它只能被赋值某个样子的函数:
interface sum { (x: number, y: number): number; } let mySum: sum; 复制代码
上面的接口定义与函数声明使用的定义很相像,都是一个圆括号包住输入并分别定义类型,然后在括号外使用一个冒号定义输出的类型。
基本类型操作
类型断言
在联合类型中,还没被赋值的变量只能使用类型公共的属性与方法,而使用类型断言则可以使用联合类型中某个类型的属性与方法:
let something: string | number; something.length; // 会报错 <string>something).length; // 不会报错 复制代码
类型断言的使用方法就是在变量前面使用尖括号断言一个类型。如果没有类型断言,因为something的类型为string与number的联合类型,所以不能访问只有string才有的length属性,但是通过类型断言就可以访问了。
类型断言的作用就是明确告诉ts这个变量的类型。类型断言还有另外一种书写形式:
<string>something === something as string 复制代码
关于类型断言,需要记住它的通项:
as
类型断言很关键的一点就是不要与泛型搞混。
类型别名
类型别名使用 type
操作符。
类型别名的作用就是用一个简短的变量来存储一长传的类型定义。类型与类型之间也是可以使用运算符执行逻辑运算的,比如联合类型的 |
操作符。下面是一个使用类型别名的例子:
type numstr = number | string; let a: numstr = 2; a = "tom"; 复制代码
在函数表达式的类型中,我们使用了一串很复杂的东西来表示一个函数变量的类型:
let sum: (x: number, y: number) => number; 复制代码
我们可以使用一个别名来简化它:
type sumFun = (x: number, y: number) => number; let sum: sumFun; 复制代码
类型别名可以理解成声明一个类型变量的方式,使用一个变量来保存一个具体的类型。不要把类型别名 type
与接口 interface
搞混。
字符串字面量类型
字符串字面量也使用 type
操作符。
字符串字面量的作用就是把一个变量的类型限定为某几个特定的字符串,一个类型为string的变量可以保存任何字符串,但是字符串字面量类型只能保存特定的字符串中的一个。
type EventNames = 'click' | 'scroll' | 'mousemove'; let event: EventNames = 'click' let event: EventNames = 'click2' // 会报错 复制代码
类型总结
我们已经知道了最基本的7个类型:
- number
- string
- boolean
- null
- undefined
- void
- any
以及由他们组成的一些高级类型:
- interface定义的接口
- 数组
- 函数
- type定义的别名与字符串字面量
高级类型操作:泛型
我们可能会遇到一个问题:
- 一个函数,我们需要输入一些数据
- 要将这些数据变成一个数组输出
- 希望输出数组元素的类型与输入数据的类型相同
- 而具体是什么类型要在使用这个函数的时候才能确定
如果我们给每个数据类型分别写一个函数,那么显然浪费了很多资源来做重复的工作。如果把数据类型写成any,则无法保证输出数组的元素与输入类型相同。因此我们希望,能有个方式告诉这个函数,我们需要它把输入输出限制成什么类型。这就是泛型的作用。
比如上面提到的例子,使用泛型来实现的代码如下:
function createArr<T> (x: T, y: T): T[] { return [x, y]; } 复制代码
泛型的使用相当于给了函数另外一套输入参数,可以输入一些类型变量,并在函数中使用。上面的T只是一个示例,它只是一个泛型的形参,可以与其他形参一样写成任何满足要求的样子。
掘金社区为本文唯一发布平台,转载请注明出处。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。