1、TypeScript 简介
1.1、什么是 TypeScript
TypeScript 不是一门全新的语言,TypeScript 是 JavaScript 的超集,它对 JavaScript 进行了一些规范和补充。所以,学习 TypeScript 需要有 JavaScript 的基础。
TypeScript 的特性:
TypeScript 是 JavaScript 的超集,它可以编译成纯 JavaScript 。TypeScript 基于 ECMAScript 标准进行拓展,支持 ECMAScript 未来提案中的特性,如装饰器、异步功能等。TypeScript 编译的 JavaScript 可以在任何浏览器运行,TypeScript 编译工具可以运行在任何操作系统上。TypeScript 起源于开发较大规模 JavaScript 应用程序的需求。由微软在2012年发布了首个公开版本。
1.2、为什么要使用 TypeScript
静态类型
JavaScript 只会在运行时去做数据类型检查,而 TypeScript 作为静态类型语言,其数据类型是在编译期间确定的,编写代码的时候要明确变量的数据类型。使用 TypeScript 后,就不会出现因为数据类型错误导致的代码运行异常。
常见的 JavaScript 异常有:
Uncaught TypeError: Cannot read property
TypeError: ‘undefined’ is not an object
TypeError: null is not an object
TypeError: Object doesn’t support property
TypeError: ‘undefined’ is not a function
TypeError: Cannot read property ‘length’
三大框架支持
TypeScript 之所以能够流行起来并且保持长久的生命力,离不开前端三大框架的支持。
Angular 是 TypeScript 最早的支持者,Angular 官方推荐使用 TypeScript 来创建应用。React 虽然经常与 Flow 一起使用,但是对 TypeScript 的支持也很友好。Vue3.0 正式版即将发布,由 TypeScript 编写。
从国内的氛围来看,由前端三大框架引领的 TypeScript 热潮已经涌来,很多招聘要求上也都有了 TypeScript 的身影。
兼容 JavaScript
TypeScript 虽然严谨,但没有丧失 JavaScript 的灵活性,TypeScript 非常包容:
TypeScript 通过 tsconfig.json 来配置对类型的检查严格程度。- 可以把
.js 文件直接重命名为 .ts 。 - 可以通过将类型定义为
any 来实现兼容任意类型。 - 即使
TypeScript 编译错误,也可以生成 JavaScript 文件。
这里先简单介绍下 any 类型,后续会详细讲解。比如一个 string 类型,在赋值过程中类型是不允许改变的:
let brand: string = 'imooc'
brand = 1
代码块12
如果是 any 类型,则允许被赋值为任意类型,这样就跟我们平时写 JavaScript 一样了:
let brand: any = 'imooc'
brand = 1
代码块12
基于上面这些特点,一个熟悉 JavaScript 的工程师,在查阅一些 TypeScript 语法后,即可快速上手 TypeScript 。
2、安装 TypeScript
2.1、使用 tsc 命令编译 TypeScript 代码
安装命令:
$ npm install -g typescript
$ cnpm i -g typescript
查看当前 typescript 版本:
$ tsc -v
创建 index.ts 文件,示例代码如下:
const msg:string = 'hello'
console.log(msg);
在当前文件所在目录运行命令:
$ tsc index.ts
会生成一个同名的 index.js 文件,然后在使用 node 命令运行 index.js 文件:
$ node index.js
运行成功即可以在控制台打印 hello 的内容。
2.2、使用 ts-node 快速编译 TypeScript
安装 ts-node :
$ npm install -g ts-node
$ cnpm i -g ts-node
使用 ts-node 可以快速运行 .ts 文件,无需先转成 .js 文件后再运行,使用 ts-node 运行 index.ts 文件:
$ ts-node index.ts
如果运行 ts-node 命令报错,提示:Error: Cannot find module '@types/node/package.json' ,需要安装相关依赖:
$ npm install -g tslib @types/node
2.3、使用工程化编译方案
创建 ts-project 项目的目录,在当前项目根目录下启动命令行工具,初始化 package.json 文件:
$ npm init -y
在 package.json 中入口文件选项改为 scr/index.ts :
{
"main": "src/index.ts"
}
然后使用 tsc 命令进行初始化:
$ tsc --init
在项目根目录下就会自动创建一个 tsconfig.json 文件,它指定了用来编译这个项目的根文件和编译选项。
对 tsconfig.json 配置文件做修改:
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"outDir": "./lib",
"rootDir": "./src",
"declaration": true,
"strict": true,
"strictNullChecks": false,
"noImplicitThis": true
},
"exclude": ["node_modules", "lib", "**/*.test.ts"],
"include": ["src"]
}
在 package.json 文件中,加入 script 命令以及依赖关系:
{
"name": "ts-project",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"tsc": "tsc"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^13.1.1",
"typescript": "^3.7.4"
}
}
下载相关依赖:
$ npm install
你会看到多了一个 node_modules 文件夹和一个 package-lock.json 文件,node_modules 文件夹是项目的所有依赖包,package-lock.json 文件将项目依赖包的版本锁定,避免依赖包大版本升级造成不兼容问题。
在 src/index.ts 文件中编写 TypeScript 代码:
export enum TokenType {
ACCESS = 'accessToken',
REFRESH = 'refreshToken'
}
执行编译命令:
$ npm run tsc
这时候可以看到多了一个 lib 文件夹,里面的内容就是项目的编译结果了。
lib 文件夹中会生成两个文件:
.d.ts 是声明文件,用于编写第三方类库,通过配置 tsconfig.json 文件中的 declaration 为 true ,在编译时可自行生成。
3、TypeScript 基本数据类型
3.1、变量声明
TypeScript 是 JavaScript 的超集,同 JavaScript 一样,声明变量可以采用下面三个关键字:
详细解释可以查看 ES6 的相关教程。
在 TypeScript 中变量声明语法:冒号 : 前面是变量名称,后面是变量类型。例如:
let i:number = 1
3.2、TypeScript 基本类型
TypeScript 中的类型有:
- 原始类型
boolean number string void null undefined bigint symbol - 元组
tuple - 枚举
enum - 任意
any unknown never - 数组
Array - 对象
object
布尔类型
表示逻辑值:true 和 false ,声明一个布尔类型:
const boo: boolean = false
boo = 0
const done: boolean = Boolean(0)
数字类型
表示双精度 64 位浮点值,可以用来表示整数和分数。
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
字符串类型
一个字符串系列,使用单引号(' )或双引号(" )来表示字符串类型,反引号(`)来定义多行文本和内嵌表达式。
let name: string = "tom";
let age: number = 5;
let words: string = `我叫 ${ name },今年 ${ age } 岁`;
任意类型
声明为 any 的变量可以赋值任意类型的值。
let str:any = 'hello'
str = 12
str = true
str = [1,2,3,4]
任意值是 TypeScript 针对编程时类型不明确的变量使用的一种数据类型,常用于以下三种情况
1、变量的值会动态改变时,比如来自用于的输入,任意类型可以让这些变量跳过编译阶段的类型检查,示例代码如下:
let x: any = 1;
x = 'I am who I am';
x = false;
2、改写现有代码时,任意类型允许在编译时可选择地包含或移除类型检查,示例代码如下:
let x: any = 4;
x.ifItExists();
x.toFixed();
3、定义存储各种类型数据的数组时,示例代码如下:
let arrayList: any[] = [1, false, 'fine'];
arrayList[1] = 100;
联合类型
如果想要为一个变量赋值多种指定的类型,在声明变量时可以使用 | 为该变量指定多种联合类型。
let numOrStr:number | string = 'hello'
numOrStr = 12
numOrStr = true
数组类型
声明变量为数组。
let arr: number[] = [1, 2];
arr.push('hello')
let arr: Array<number> = [1, 2];
arr = [1,2,'hello']
let list: any[] = ['Sherlock', 1887]
元组
元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。
let x: [string, number];
x = ['hello', 1];
x = [1, 'hello'];
x = ['hello', 1, true];
console.log(x[0]);
枚举
枚举类型用于定义数值集合。
数字枚举:
enum Color{
Blue,
Red,
Green
}
console.log(Color.Blue);
let a:Color = Color.Blue
console.log(a)
console.log(Color[0]);
可以为数字枚举手动赋值:
enum Color{
Blue = 10,
Red,
Green
}
console.log(Color.Blue);
console.log(Color.Red);
console.log(Color[0]);
字符串枚举:
enum Color{
Blue = 'BLUE',
Red = 'RED',
Green = 'GREEN'
}
console.log(Color.Blue);
console.log(Color.Red);
let str = 'RED'
console.log(str === Color.Red);
常量枚举
没有使用常量枚举之前编译的效果:
enum Color{
Blue = "BLUE",
Red = 'RED',
Green = 'GREEN'
}
console.log(Color.Blue);
编译后:
var Color;
(function (Color) {
Color["Blue"] = "BLUE";
Color["Red"] = "RED";
Color["Green"] = "GREEN";
})(Color || (Color = {}));
console.log(Color.Blue);
使用常量枚举:
const enum Color{
Blue = "BLUE",
Red = 'RED',
Green = 'GREEN'
}
console.log(Color.Blue);
编译后:
console.log("BLUE" );
使用常量枚举可以提升编译的性能,不会将 ts 中的枚举编译成 JavaScrit 代码,而是直接使用常量值,这样就提高了性能。只有常量值才能使用常量枚举。
void
用于表示方法返回值的类型,表示该方法没有返回值,是一个返回值的占位符。
function hello(): void {
alert("Hello Runoob");
}
声明一个 void 类型的变量没有什么用,只能为变量赋值 undefined 和 null 。
let nothing: void = undefined
null
表示对象值缺失。
在 JavaScript 中 null 表示 “什么都没有”。null 是一个只有一个值的特殊类型。表示一个空对象引用。
用 typeof 检测 null 返回是 object 。
undefined
用于初始化变量为一个未定义的值。
在 JavaScript 中, undefined 是一个没有设置值的变量。typeof 一个没有值的变量会返回 undefined 。
Null 和 Undefined 是其他任何类型(包括 void )的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 null 或 undefined 。而在 TypeScript 中启用严格的空校验( --strictNullChecks )特性,就可以使得 null 和 undefined 只能被赋值给 void 或本身对应的类型,示例代码如下:
let x: number;
x = 1;
x = undefined;
x = null;
上面的例子中变量 x 只能是数字类型。如果一个类型可能出现 null 或 undefined , 可以用 | 来支持多种类型,示例代码如下:
let x: number | null | undefined;
x = 1;
x = undefined;
x = null;
never
never 是其他类型(包括 null 和 undefined )的子类型,代表从不会出现的值。这意味着声明为 never 类型的变量只能被 never 类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环),示例代码如下:
let x: never;
let y: number;
x = 123;
x = (()=>{ throw new Error('exception')})();
y = (()=>{ throw new Error('exception')})();
function error(message: string): never {
throw new Error(message);
}
function loop(): never {
while (true) {}
}
注意事项:
TypeScript 中描述类型要用 小写 ,比如 boolean 、number 、string 等;- 大写开头的如
Boolean 、Number 、String 代表的是 JavaScript 的构造函数;
let a: Number = new Number('10')
let b: number = Number('10')
a instanceof Number
b instanceof Number
__proto__ 是非标准属性,你也可以使用 Object.getPrototypeOf() 方法来访问一个对象的原型:
a.__proto__ === Object.getPrototypeOf(a)
需要注意的是,TypeScript 一些基本类型,需要记住的是:TypeScript 中描述类型要用 小写。不要滥用 any !
3.3、BigInt 类型
概念
bigint 是一种基本数据类型(primitive data type )。
JavaScript 中可以用 Number 表示的最大整数为 2^53 - 1 ,可以写为 Number.MAX_SAFE_INTEGER 。如果超过了这个界限,可以用 BigInt 来表示,它可以表示任意大的整数。
语法
在一个整数字面量后加 n 的方式定义一个 BigInt ,如:10n 或者调用函数 BigInt() :
const theBiggestInt: bigint = 9007199254740991n
const alsoHuge: bigint = BigInt(9007199254740991)
const hugeString: bigint = BigInt("9007199254740991")
theBiggestInt === alsoHuge
theBiggestInt === hugeString
BigInt 与 Number 的不同点:
BigInt 不能用于 Math 对象中的方法。BigInt 不能和任何 Number 实例混合运算,两者必须转换成同一种类型。BigInt 变量在转换为 Number 变量时可能会丢失精度。
使用 number 类型:
const biggest: number = Number.MAX_SAFE_INTEGER
const biggest1: number = biggest + 1
const biggest2: number = biggest + 2
biggest1 === biggest2
使用 BigInt 类型:
const biggest: bigint = BigInt(Number.MAX_SAFE_INTEGER)
const biggest1: bigint = biggest + 1n
const biggest2: bigint = biggest + 2n
biggest1 === biggest2
类型信息
使用 typeof 检测类型时,BigInt 对象返回 bigint :
typeof 10n === 'bigint'
typeof BigInt(10) === 'bigint'
typeof 10 === 'number'
typeof Number(10) === 'number'
运算
BigInt 可以正常使用 + 、- 、* 、/ 、** 、% 符号进行运算:
const previousMaxSafe: bigint = BigInt(Number.MAX_SAFE_INTEGER)
const maxPlusOne: bigint = previousMaxSafe + 1n
const multi: bigint = previousMaxSafe * 2n
const subtr: bigint = multi – 10n
const mod: bigint = multi % 10n
const bigN: bigint = 2n ** 54n
当使用 / 操作符时,会向下取整,不会返回小数部分:
const divided: bigint = 5n / 2n
比较 与 条件
Number 和 BigInt 可以进行比较:
0n === 0
0n == 0
1n < 2
2n > 1
2 > 2
2n > 2
2n >= 2
条件判断:
if (0n) {
console.log('条件成立!');
} else {
console.log('条件不成立!');
}
0n || 10n
0n && 10n
Boolean(0n)
Boolean(10n)
!10n
!0n
3.4、Symbol 类型
概念
symbol 是一种基本数据类型(primitive data type )。
Symbol() 函数会返回 symbol 类型的值。每个从 Symbol() 返回的 symbol 值都是唯一的。
语法
Symbol([description])
参数 description :可选的,字符串类型。
使用 Symbol() 创建新的 symbol 类型:
const sym1: symbol = Symbol()
const sym2: symbol = Symbol('foo')
const sym3: symbol = Symbol('foo')
上面的代码创建了三个新的 symbol 类型,但要注意每个从 Symbol() 返回的值都是唯一的:
console.log(sym2 === sym3)
每个 Symbol() 方法返回的值都是唯一的,所以,sym2 和 sym3 不相等。
Symbol() 作为构造函数是不完整的:
const sym = new Symbol()
这种语法会报错,是因为从 ECMAScript 6 开始围绕原始数据类型创建一个显式包装器对象已不再被支持,但因历史遗留原因, new Boolean() 、new String() 以及 new Number() 仍可被创建:
const symbol = new Symbol()
const bigint = new BigInt()
const number = new Number()
const boolean = new Boolean()
const string = new String()
应用场景
1、当一个对象有较多属性时(往往分布在不同文件中由模块组合而成),很容易将某个属性名覆盖掉,使用 Symbol 值可以避免这一现象,比如 vue-router 中的 name 属性。
export const aRouter = {
path: '/index',
name: Symbol('index'),
component: Index
},
export const bRouter = {
path: '/home',
name: Symbol('index'),
component: Home
},
import { aRouter } from './a.js'
import { bRouter } from './b.js'
const routes = [
aRouter,
bRouter
]
两个不同文件使用了同样的 Symbol('index') 作为属性 name 的值,因 symbol 类型的唯一性,就避免了重复定义。
2、模拟类的私有方法。
const permission: symbol = Symbol('permission')
class Auth {
[permission]() {
}
}
这种情况通过类的实例是无法取到该方法,模拟类的私有方法。
但是,TypeScript 是可以使用 private 关键字的,所以这种方法可以在 JavaScript 中使用。
3、判断是否可以用 for...of 迭代。
if (Symbol.iterator in iterable) {
for(let n of iterable) {
console.log(n)
}
}
4、Symbol.prototype.description
Symbol([description]) 中可选的字符串即为这个 Symbol 的描述,如果想要获取这个描述:
const sym: symbol = Symbol('imooc')
console.log(sym);
console.log(sym.toString());
console.log(sym.description);
3.5、元组(Tuple )
概念
相同类型元素组成成为数组,不同类型元素组成了元组(Tuple )。
语法
声明一个由 string 和 number 构成的元组:
const list: [string, number] = ['Sherlock', 1887]
const list1: [string, number] = [1887, 'Sherlock']
元组中规定的元素类型顺序必须是完全对照的,而且不能多、不能少,list1 中定义的第一个元素为 string 类型,不能赋值为 number 类型的数据。
当赋值或访问一个已知索引的元素时,会得到正确的类型:
const list: [string, number] = ['Sherlock', 1887]
list[0].substr(1)
list[1].substr(1)
要注意元组的越界问题,虽然可以越界添加元素(不建议),但是不可越界访问:
const list: [string, number] = ['Sherlock', 1887]
list.push('hello world')
console.log(list)
console.log(list[2])
可选元素类型
元组类型允许在元素类型后缀一个 ? 来说明元素是可选的:
const list: [number, string?, boolean?]
list = [10, 'Sherlock', true]
list = [10, 'Sherlock']
list = [10]
可选元素必须在必选元素的后面,也就是如果一个元素后缀了 ? 号,其后的所有元素都要后缀 ? 号。
元组类型的 Rest 使用
元组可以作为参数传递给函数,函数的 Rest 形参可以定义为元组类型:
declare function rest(...args: [number, string, boolean]): void
等价于:
declare function rest(arg1: number, arg2: string, arg3: boolean): void
还可以这样:
const list: [number, ...string[]] = [10, 'a', 'b', 'c']
const list1: [string, ...number[]] = ['a', 1, 2, 3]
Rest 元素指定了元组类型是无限扩展的,可能有零个或多个具有数组元素类型的额外元素。
关键字 declare 表示声明作用
3.6、枚举(Enum )
语法
使用枚举我们可以定义一些带名字的常量。TypeScript 支持数字的和基于字符串的枚举。枚举类型弥补了 JavaScript 的设计不足,很多语言都拥有枚举类型。
当我们需要一组相同主题下的数据时,枚举类型就很有用了。
enum Direction { Up, Down, Left, Right }
enum Months { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }
enum Size { big = '大', medium = '中', small = '小' }
enum Agency { province = 1, city = 2, district = 3 }
代码中通过枚举类型分别声明了:不同的 方向 ,一年内不同的 月份 ,一个商品的不同 尺寸属性 ,经销商的不同 级别 ,这样的一组常量数据,是在一个相同主题下的不同表示。
数字枚举
声明一个枚举类型,如果没有赋值,它们的值默认为数字类型且从 0 开始累加:
enum Months {
Jan,
Feb,
Mar,
Apr
}
Months.Jan === 0
Months.Feb === 1
Months.Mar === 2
Months.Apr === 3
现实中月份是从 1 月开始的,那么只需要这样:
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
Months.Jan === 1
Months.Feb === 2
Months.Mar === 3
Months.Apr === 4
字符串枚举
枚举类型的值为字符串类型:
enum TokenType {
ACCESS = 'accessToken',
REFRESH = 'refreshToken'
}
console.log(TokenType.ACCESS === 'accessToken')
console.log(TokenType['REFRESH'] === 'refreshToken')
枚举的取值,有 TokenType.ACCESS 和 TokenType['ACCESS'] 这两种不同的写法,效果是相同的。
数字类型和字符串类型可以混合使用,但是不建议:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
计算常量成员
枚举类型的值可以是一个简单的计算表达式:
enum Calculate {
a,
b,
expired = 60 * 60 * 24,
length = 'imooc'.length,
plus = 'hello ' + 'world'
}
console.log(Calculate.expired)
console.log(Calculate.length)
console.log(Calculate.plus)
反向映射
所谓的反向映射就是指枚举的取值,不但可以正向的 Months.Jan 这样取值,也可以反向的 Months[1] 这样取值。
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
将上面的代码进行编译,查看编译后的 JavaScript 代码:
'use strict'
var Months;
(function (Months) {
Months[Months['Jan'] = 1] = 'Jan'
Months[Months['Feb'] = 2] = 'Feb'
Months[Months['Mar'] = 3] = 'Mar'
Months[Months['Apr'] = 4] = 'Apr'
})(Months || (Months = {}))
通过查看编译后的代码,可以得出:
console.log(Months.Mar === 3)
console.log(Months[3])
console.log(Months.Mar === 3)
console.log(Months[3] === 'Mar')
const 枚举
在枚举上使用 const 修饰符:
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
const month = Months.Mar
查看一下编译后的内容:
'use strict'
const month = 3
发现枚举类型应该编译出的对象没有了,只剩下 month 常量。这就是使用 const 关键字声明枚举的作用。因为变量 month 已经使用过枚举类型,在编译阶段 TypeScript 就将枚举类型抹去,这也是性能提升的一种方案。
枚举合并
分开声明名称相同的枚举类型,会自动合并:
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
enum Months {
May = 5,
Jun
}
console.log(Months.Apr)
console.log(Months.Jun)
编译后的 JavaScript 代码:
'use strict'
var Months;
(function (Months) {
Months[Months['Jan'] = 1] = 'Jan'
Months[Months['Feb'] = 2] = 'Feb'
Months[Months['Mar'] = 3] = 'Mar'
Months[Months['Apr'] = 4] = 'Apr'
})(Months || (Months = {}));
(function (Months) {
Months[Months['May'] = 5] = 'May'
Months[Months['Jun'] = 6] = 'Jun'
})(Months || (Months = {}))
console.log(Months.Apr)
console.log(Months.Jun)
3.7、Never 类型
概念
never 类型表示那些永不存在的值的类型。
never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never 。
应用场景
一个抛出异常的函数表达式,其函数返回值类型为 never :
function error(message:string): never {
throw new Error(message)
}
同样的,不会有返回值的函数表达式,其函数返回值类型也为 never :
function fail(): never {
return error("Something failed")
}
不能取得值的地方:
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
function handleValue(val: All) {
switch (val.type) {
case 'foo':
break
case 'bar':
break
default:
const exhaustiveCheck: never = val
break
}
}
3.8、unknown 类型
any 无需事先执行任何类型的检查:
let value: any
value = true
value = 10
value = "Hello World"
value = []
value = {}
value = Math.random
value = null
value = undefined
value = new TypeError()
value = Symbol('name')
value.foo.bar
value.trim()
value()
new value()
value[0][1]
在许多情况下,这太宽松了。如果使用了 unknown 类型:
let value: unknown
value = true
value = 10
value = "Hello World"
value = []
value = {}
value = Math.random
value = null
value = undefined
value = new TypeError()
value = Symbol('name')
所有对该 value 变量的分配都被认为是类型正确的。
可以尝试:
let value: unknown
let value1: unknown = value
let value2: any = value
let value3: boolean = value
let value4: number = value
let value5: string = value
let value6: object = value
let value7: any[] = value
可以看到,该 unknown 类型只能分配给 any 类型和 unknown 类型本身。
继续尝试:
let value: unknown
value.foo.bar
value.trim()
value()
new value()
value[0][1]
unknown 类型在被确定为某个类型之前,不能被进行诸如函数执行、实例化等操作,一定程度上对类型进行了保护。
在那些将取得任意值,但不知道具体类型的地方使用 unknown ,而非 any 。
4、接口(Interface )
4.1、接口的概念
接口是对 JavaScript 本身的随意性进行约束,通过定义一个接口,约定了变量、类、函数等应该按照什么样的格式进行声明,实现多人合作的一致性。TypeScript 编译器依赖接口用于类型检查,最终编译为 JavaScript 后,接口将会被移除。
interface DemoInterface {
}
4.2、应用场景
在声明一个对象、函数或者类时,先定义接口,确保其数据结构的一致性。
在多人协作时,定义接口尤为重要。
4.3、接口的好处
过去我们写 JavaScript 定义一个函数:
function getClothesInfo(clothes) {
console.log(clothes.price)
}
let myClothes = {
color: 'black',
size: 'XL',
price: 98
}
getClothesInfo(myClothes)
之前我们写 JavaScript 这样是很正常的,但同时你可能会遇到下面这些问题:
getClothesInfo()
getClothesInfo({ color: 'black' })
相信原因你也知道,JavaScript 是 弱类型 语言,并不会对传入的参数进行任何检测,错误在运行时才被发现。那么通过定义 接口 ,在编译阶段甚至开发阶段就避免掉这类错误,接口将检查类型是否和某种结构做匹配。
下面通过接口的方式重写之前的例子:
interface Clothes {
color: string;
size: string;
price: number;
}
function getClothesInfo(clothes: Clothes) {
console.log(clothes.price)
}
let myClothes: Clothes = {
color: 'black',
size: 'XL',
price: 98
}
getClothesInfo(myClothes)
代码中,定义了一个接口 Clothes ,在传入的变量 clothes 中,它的类型为 Clothes 。这样,就约束了这个传入对象的 外形 与接口定义一致。只要传入的对象满足上面的类型约束,那么它就是被允许的。
- 定义接口要 首字母大写。
- 只需要关注值的 外形,并不像其他语言一样,定义接口是为了实现。
- 如果没有特殊声明,定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致。
4.4、接口的属性
可选属性
接口中的属性不全是必需的。可选属性的含义是该属性在被变量定义时可以不存在。
interface Clothes {
color?: string;
size: string;
price: number;
}
let myClothes: Clothes = {
size: 'XL',
price: 98
}
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。这时,仍不允许添加未定义的属性,如果引用了不存在的属性时 TS 将直接捕获错误。
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly 来指定只读属性,比如价格是不能被修改的:
interface Clothes {
color?: string;
size: string;
readonly price: number;
}
let myClothes: Clothes = {
size: 'XL',
price: 98
}
myClothes.price = 100
TypeScript 可以通过 ReadonlyArray<T> 设置数组为只读,那么它的所有写方法都会失效。
let arr: ReadonlyArray<number> = [1,2,3,4,5];
arr[0] = 6;
最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。做为 变量 使用的话用 const ,若做为 属性 则使用 readonly 。
任意属性
有时候我们希望接口允许有任意的属性,语法是用 [] 将属性包裹起来:
interface Clothes {
color?: string;
size: string;
readonly price: number;
[propName: string]: any;
}
let myClothes: Clothes = {
size: 'XL',
price: 98,
activity: 'coupon'
}
这里的接口 Clothes 可以有任意数量的属性,并且只要它们不是 color size 和 price ,那么就无所谓它们的类型是什么。
案例:
使用 axios 库发起 HTTP 传输的时候,可以写入一个自定义的属性,就是因为源码中定义了一个任意属性:
this.$axios({
method: 'put',
url: '/cms/user',
data: {
nickname: this.nickname,
},
showBackend: true,
})
4.5、函数类型
除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有 参数列表 和 返回值类型 的函数定义。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean {
return source.search(subString) > -1;
}
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。你可以改变函数的参数名,只要保证函数参数的位置不变。函数的参数会被逐个进行检查:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
return src.search(sub) > -1;
}
如果你不想指定类型,TypeScript 的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc 类型变量。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
如果接口中的函数类型带有函数名,下面两种书写方式是等价的:
interface Calculate {
add(x: number, y: number): number
multiply: (x: number, y: number) => number
}
4.6、可索引类型
代码示例:
let arr = [1, 2, 3, 4, 5]
let obj = {
brand: 'imooc',
type: 'education'
}
arr[0]
obj['brand']
定义可索引类型接口:
interface ScenicInterface {
[index: number]: string
}
let arr: ScenicInterface = ['西湖', '华山', '故宫']
let favorite: string = arr[0]
示例中索引签名是 number类型 ,返回值是字符串类型。
另外还有一种索引签名是 字符串类型 。我们可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。通过下面的例子理解这句话:
interface Foo {
[index: string]: number;
x: number;
y: number;
}
interface Bar {
[index: string]: number;
x: number;
y: string;
}
第 12 行,语法错误是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。也就是说用 100 (一个number )去索引等同于使用"100"(一个string )去索引,因此两者需要保持一致。
4.7、类类型
我们希望类的实现必须遵循接口定义,那么可以使用 implements 关键字来确保兼容性。
这种类型的接口在传统面向对象语言中最为常见,比如 java 中接口就是这种类类型的接口。这种接口与抽象类比较相似,但是接口只能含有抽象方法和成员属性,实现类中必须实现接口中所有的抽象方法和成员属性。
interface AnimalInterface {
name: string;
}
class Dog implements AnimalInterface {
name: string;
constructor(name: string){
this.name = name
}
}
你也可以在接口中描述一个方法,在类里实现它:
interface AnimalInterface {
name: string
eat(m: number): string
}
class Dog implements AnimalInterface {
name: string;
constructor(name: string){
this.name = name
}
eat(m: number) {
return `${this.name}吃肉${m}分钟`
}
}
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
4.8、继承接口
和类一样,接口也可以通过关键字 extents 相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
一个接口可以继承多个接口,创建出多个接口的合成接口。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
4.9、混合类型
接口可以描述函数、对象的方法或者对象的属性。
有时希望一个对象同时具有上面提到多种类型,比如一个对象可以当做函数使用,同时又具有属性和方法。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function (start: number) { } as Counter;
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
上面代码中,第 1 行,声明一个接口,如果只有 (start: number): string 一个成员,那么这个接口就是函数接口,同时还具有其他两个成员,可以用来描述对象的属性和方法,这样就构成了一个混合接口。
第 7 行,创建一个 getCounter() 函数,它的返回值是 Counter 类型的。
let counter = function (start: number) { } as Counter;
第 8 行,通过类型断言,将函数对象转换为 Counter 类型,转换后的对象不但实现了函数接口的描述,使之成为一个函数,还具有 interval 属性和 reset() 方法。断言成功的条件是,两个数据类型只要有一方可以赋值给另一方,这里函数类型数据不能赋值给接口类型的变量,因为它不具有 interval 属性和 reset() 方法。
创建 React 项目
创建项目
执行下面的命令新建一个名字为 demo 的 React 项目:
npx create-react-app demo --template typescript
根据 typescript 官网文档的说明,还可以使用下面的命令:
npx create-react-app demo --scripts-version=react-scripts-ts
react-script-ts 是一个在采用了标准的 create-react-app 项目流程 的基础上,混合了 TypeScript 的功能的集合。
我想,原来是采用的第二种方式,后来就整理为了第一种方式,这里采用第一种方式。
创建的项目文件结构:
demo02-ts-template/
|─ node_modules/
|─ public/
| └─ favicon.ico
| └─ index.html
| └─ manifest.json
| └─ ...
|─ src/
| └─ ...
|─ .gitignore
|─ package.json
|─ package-lock.json
|─ README.md
└─ tsconfig.json
执行:
npm start
运行项目,默认服务将运行在 localhost:3000 。
|