TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。
TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。
如果你的本地环境已经安装了 npm 工具,可以使用以下命令来安装。
使用国内镜像:
npm config set registry https://registry.npmmirror.com
安装 typescript:
npm install -g typescript # -g 是代表全局安装,不改变环境就使用cnmp命令(前提是已安装)
#使用淘宝镜像的命令:
npm install -g cnpm --registry=https://registry.npmmirror.com # cnpm命令安装
如果出现以下错误:
npm err! Error: connect ECONNREFUSED 127.0.0.1:8087
解决办法为:
npm config set proxy null
安装完成后我们可以使用 tsc 命令来执行 TypeScript 的相关代码,以下是查看版本号:
$ tsc -v
Version 5.3.3
如果安装报错大概率就是权限问题,请使用管理员的命令行模式
然后我们的ts文件是不能够在浏览器当中进行解析的,我们的ts文件首先需要通过编译,编译之后才能够被浏览器进行解析,我们直接使用命令
// tsc + 文件名
tsc demo.ts
编译之后我们的目录下面会产生一个和ts文件同名的js文件,之后我们就可以直接使用这个编译得到的js即可。我们安装好了node,就可以直接使用node命令对js文件进行解析了,使用node命令:
// node + 文件名
node demo.js
插件市场安装即可
tsc命令不仅仅可以去编译指定某个ts文件,它还可以去编译整个项目,编译整个工程。
tsc --init
target:作用就是用设置编译过后 Javascript 所采用的 ECMA 标准;
module:输出的代码采用什么样的方式去进行模块化;
outDir:设置编译结果输出到的文件夹,一般我们会输出到 dist 文件夹;
rootDir:配置我们源代码,也就是 Typescript 的代码所在的文件夹,一般我们会把源代码放在src目录;
sourceMap:开启源代码映射,开启之后,调试的时候可以 sourceMap 文件进行调试源代码;
strict:开启所有严格检查选项,严格模式下,需要我们对每一个成员都要指定明确的类型等等;
需要注意的是,如果我们还是使用 tsc 编译某个 ts文件时,配置文件是不起作用的,只有当我们直接运行tsc命令去编译整个项目时,配置文件才生效。
{
/**
* 用来指定哪些ts需要被编译 ** 表示任意目录 * 表示任意文件
*/
"include": [
"./"
],
"exclude": [
"./demo.ts"
],
"compilerOptions": {
// 编译的js版本目标
"target": "ES6",
// 模块化版本
"module": "system",
// 指定项目中要使用到的库
"lib": ["esnext"],
// 编译后所在目录
"outDir": "./dist",
// 将编译的js合并到一起
"outFile": "./dist/app.js",
// 是否对js进行编译,false表示不对js进行编译
"allowJs": false,
// 是否对js文件进行检验,false表示不检验
"checkJs": false,
// 是否移除注释,false表示不去掉注释
"removeComments": false,
// 不生成编译文件,
"noEmit": false,
// 当编译出错时不生成编译文件
"noEmitOnError": false,
// 所有严格模式的总开关
"strict": false,
// 是否开启严格模式,false表示不开启
"alwaysStrict": false,
// 当一个变量不指定类型时,默认使用any,设置为true时,表示不允许使用隐式any
"noImplicitAny": false,
// 设置为true,表示不允许使用隐式this
"noImplicitThis": false,
}
}
(function () {
const a: string = 'foobar'
})()
解决方式二:
const a: string = 'foobar'
export {}
注意export后面的{ },是export 的语法,并不是表示导出一个空对象
const a: string = 'foobar'
const b: number = 100 // 包括NaN Infinity
const c: boolean = true // false
跟 flow 不同,以上这三种类型在非严格模式
(strictNullChecks)是允许为空的,也就是说我们可以赋值为 null
或者是 undefined
。
const a: string = null // undefined
const b: number = null // undefined
const c: boolean = null // undefined
但需要注意的是:在严格模式下是不允许的.
const e: void = undefined
const f: null = null
const g: undefined = undefined
Symbol 是 ES2015 标准中定义的成员,使用它的前提是必须确保有对应的 ES2015 标准库引用,也就是 tsconfig.json 中的 lib 选项必须包含 ES2015 。
配置tsconfig.json
{
"compilerOptions": {
"lib": ["es2015"]
}
}
定义symbol类型
const h: symbol = Symbol()
Typescript 中 Object 类型不单是指普通对象类型,它泛指所有的非原始类型,也就是对象,数组还有函数。
// 注意这里的 object 类型首字母是小写的
// 函数
const fn: object = function () {}
// 普通对象
const obj: object = {}
// 数组
const arr: object = []
如果需要普通对象类型,就要使用类似对象字面量的语法去标记类型,这种对象类型限制,它要求我们赋值对象的结果必须要跟我们标记类型的结构完全一致,不能多,也不能少。
const obj: { foo: number, str: string } = { foo: 123, str: '123' }
更好的方式是使用接口的方式,下面会有讲哦。
TypeScript 定义数组的方式跟 flow 几乎完全一致。
使用 Array 泛型
// 元素类型选择 number,表示纯数字数组
const arr1: Array<number> = [1, 2, 3]
使用元素类型 + [ ]
// 元素类型选择 number,表示纯数字数组
const arr1: number[] = [1, 2 ,3]
元组就是一个明确元素数量以及每一个元素类型的数组。
定义元组类型的方式:可类似数组字面量
// 这个时候表示:只能存储两个对应类型的元素
const tuple: [number, string] = [18, 'zhangsan']
访问元组当中元素的方式
可以使用数组下标的方式去访问
const tuple: [number, string] = [18, 'zhangsan']
const age = tuple[0]
const name = tuple[1]
使用数组解构的方式去提取
const tuple: [number, string] = [18, 'zhangsan']
const [age, name] = tuple
元组一般用于一个函数中返回多个返回值
const entries: [string, number][] = Object.entries({
foo: 123,
bar: 456
})
const [key, value] = entries[0]
// key => foo, value => 123
枚举的介绍
枚举类型的特点
可以给一组数值取上一个更好理解的名字;
一个枚举中只会存在几个固定的值,并不会出现超出范围的可能性;
在很多编程语言中都会有枚举这种数据结构,但是在 JavaScript 中没有这种枚举数据结构,很多时候都是用一个对象来模拟枚举,如下:
const PostStatus = {
Draft: 0,
Unpublished: 1,
Published: 2
}
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // 0 // 1 // 2
}
在 Typescript 当中可以用 enum 关键词声明一个枚举,{ } 里面是枚举的值,注意的是用 = 号,使用方式跟对象一样。
enum PostStatus {
Draft = 0,
Unpublished = 1,
Published = 2
}
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // 0 // 1 // 2
}
枚举值自动基于前一个值自增,如果没指定具体数值,则从 0 开始。
enum PostStatus1 {
Draft, // 0
Unpublished, // 1
Published // 2
}
enum PostStatus2 {
Draft = 6, // 6
Unpublished, // 7
Published2 // 8
}
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus1.Draft // 0 // 1 // 2
}
字符串枚举无法自增,需要手动添加,字符串枚举不太常见。
enum PostStatus {
Draft = 'a',
Unpublished = 'b',
Published2 = 'c'
}
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // a // b // c
}
枚举类型会影响编译结果
枚举类型会入侵到我们的运行时的代码,也就是说它会影响我们编译后的结果,我们在Typescript当中使用大量的大多数类型,经过编译转换过后都会被移除掉,因为它们只是为了我们在编译过程中可以进行类型检查;但是枚举类型不会,它最终会编译成一个双向的键值对对象,目的是可以让我们动态的根据枚举值去或者枚举的名称,也就是说我们可以通过索引器的方式去访问对应的枚举名称。
编译前
enum PostStatus {
Draft,
Unpublished,
Published
}
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // 0 // 1 // 2
}
编译后
"use strict";
var PostStatus;
(function (PostStatus) {
PostStatus[PostStatus["Draft"] = 0] = "Draft";
PostStatus[PostStatus["Unpublished"] = 1] = "Unpublished";
PostStatus[PostStatus["Published"] = 2] = "Published";
})(PostStatus || (PostStatus = {}));
var post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // 0 // 1 // 2
};
如果我们确认我们代码中不会使用索引器的方式去访问枚举,那推荐使用常量枚举,常量枚举:enum 前面加个关键词 const。
编译前
const enum PostStatus {
Draft,
Unpublished,
Published
}
const post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: PostStatus.Draft // 0 // 1 // 2
}
编译后
"use strict";
var post = {
title: 'Hello TypeScript',
content: 'TypeScript is a typed superset of JavaScript.',
status: 0 /* Draft */ // 0 // 1 // 2
};
无非是对输入输出做限制,也就是参数和返回值。
基本用法
function func (a: number, b:number): string {
return 'func1'
}
func(100, 200)
需要注意,形参和实参的个数要一致
可选参数
function func (a: number, b?:number): string {
return 'func1'
}
func(100)
function func (a: number, b:number = 10): string {
return 'func1'
}
func(100)
注意:使用可选参数或者是默认参数,都必须要在参数列表的最后.
接收任意个数参数
使用 es6 的 … 操作符.
function func (a: number, ...rest: number[]): string {
return 'func1'
}
func(100, 200, 300, 400)
因为函数表达式最终是放在一个变量上的,接收这个函数的变量,也是应该有类型的.
const func = function (a: number, b: number): string {
return 'func1'
}
typescript一般会根据函数表达式推断出这个变量的类型
如果是把一个函数作为参数传递,也就是回调函数的方式,一般我们就会去约束我们这个回调函数形参的类型,使用类似箭头函数的方式去表示我们这个参数接收什么类型的函数,这种方式在定义接口的时候经常用到。
const func: (a: number, b: number) => string = function (a: number, b: number): string {
return 'func1'
}
由于 JavaScript 自身是弱类型的关系,很多内置的 API 它本身就支持接收任意类型的参数,而 Typescript 它又是基于 Javascript 的基础之上的,所以说我们难免会在代码当中需要去用一个变量接收任意类型的数据。
// 任意类型(弱类型)
function stringify (value: any) {
return JSON.stringify(value)
}
stringify('string')
stringify(100)
stringify(true)
// any 类型仍然属于动态类型,它的特点跟普通 JavaScript 变量是一样的
// 也就是可以接收任意类型的值
let foo: any = 'string'
// 在运行当中还可以接收其他类型的值
foo = 100
foo.bar()
// any 类型是不安全的
any 类型,typescript 不会去做类型检查,仍然会存在类型安全的问题。所以不要轻易去使用这种类型。
在 Typescript中,如果我们没有通过类型注解去明确一个变量的类型,那么 typescript 会根据变量的使用情况,来推断这个变量的类型,这样一种特性叫隐式类型推断。
定义一个变量赋值一个 number 类型的值,typescript会推断这个变量为 number 类型,如下:
此时我们在给变量赋值为 string 类型的话就会 typescript 就会报错提示,如下:
如果 typescript 无法推断变量的类型,就会把它当成 any 类型,如下:
虽然 Typescript 中支持隐式类型推断,而且这种隐式类型推断可以帮我们简化一部分代码,但仍然建议为每个变量添加明确的类型,方便以后我们能更好理解我们的代码。
有时候在某些特殊情况下,typescript 无法推断变量的具体类型,但是我们作为开发者,根据代码的具体情况,是可以明确知道这个变量的类型的,那么我们可以通过类型断言告诉 typescript 这个变量的类型。
这种情况我们就可以去断言 res 是个 number 类型。
使用 as 关键词(推荐)
const num1 = res as number
在变量的前面使用 <类型> 的方式断言
这种方式需要注意的是不能在 jsx 中使用
,<>
会跟 jsx 中的标签产生语法上的冲突
。
const num1 = <number>res
注意:类型断言并不是类型转换,因为类型转换是代码在运行时的概念,而类型断言它只是在编译过程当中的概念,当代码编译过后这个断言就不会存在了,所以它跟类型转换是有本质的差异的。
联合类型(Union Types)可以通过管道(|)将变量设置多种类型,赋值时可以根据设置的类型来赋值。
注意:只能赋值指定的类型,如果赋值其它类型就会报错。
创建联合类型的语法格式如下:
Type1|Type2|Type3
实例
声明一个联合类型:
var val:string|number
val = 12
console.log("数字为 "+ val)
val = "Runoob"
console.log("字符串为 " + val)
也可以将联合类型作为函数参数使用
function disp(name:string|string[])
function disp(name:string|string[]) {
if(typeof name == "string") {
console.log(name)
} else {
var i;
for(i = 0;i<name.length;i++) {
console.log(name[i])
}
}
}
disp("Runoob")
console.log("输出数组....")
disp(["Runoob","Google","Taobao","Facebook"])
var arr:number[]|string[];
即可以给arr = [1,2,4] ,也可以arr = [“Runoob”,“Google”,“Taobao”]
var arr:number[]|string[];
var i:number;
arr = [1,2,4]
console.log("**数字数组**")
for(i = 0;i<arr.length;i++) {
console.log(arr[i])
}
arr = ["Runoob","Google","Taobao"]
console.log("**字符串数组**")
for(i = 0;i<arr.length;i++) {
console.log(arr[i])
}
可以理解为一种规范或者契约,它可以用来约定对象的结构,我们去使用一个接口就要去遵循这个接口的所有约定。在 typescript 中,接口最直观的体现是,约定一个对象当中具体应该有那些成员,并且这些成员是什么类型的。
interface Post {
// 多个成员可使用 ','、';' 分割,也可以不用
title: string
content: string
}
// 对于这个函数所接收的 post 对象,他就有一定的要求
// 所传入的对象必须要有一个 title 属性和 content 属性
// 只不过这种要求实际上是隐性的,它没有明确的表达出来
// 这种情况下我们就可以使用接口去表现出来这种约束
function printPost (post: Post) {
console.log(post.title)
console.log(post.content)
}
printPost({
title: 'Hello TypeScript',
content: 'A javascript superset'
})
定义了一个接口 Post ,接着把post的数据{title:“”,content:“”},它的类型是 Post 。printPost实现了接口 IPerson 的属性和方法。
编译之后,我们没有发现任何有关接口的代码,其实 typescript 的接口只是为我们有结构的数据做类型约束,在运行阶段这种接口没有意义。
interface RunOptions {
program:string;
commandline:string[]|string|(()=>string);
}
// commandline 是字符串
var options:RunOptions = {program:"test1",commandline:"Hello"};
console.log(options.commandline)
// commandline 是字符串数组
options = {program:"test1",commandline:["Hello","World"]};
console.log(options.commandline[0]);
console.log(options.commandline[1]);
// commandline 是一个函数表达式
options = {program:"test1",commandline:()=>{return "**Hello World**";}};
var fn:any = options.commandline;
console.log(fn());
接口中我们可以将数组的索引值和元素设置为不同类型,索引值可以是数字或字符串。
设置元素为字符串类型:
interface namelist {
[index:number]:string
}
// 类型一致,正确
var list2:namelist = ["Google","Runoob","Taobao"]
// 错误元素 1 不是 string 类型
// var list2:namelist = ["Runoob",1,"Taobao"]
接口继承就是说接口可以通过其他接口来扩展自己。
Typescript 允许接口继承多个接口。
继承使用关键字 extends
。
Child_interface_name extends super_interface_name
Child_interface_name extends super_interface1_name, super_interface2_name,…,super_interfaceN_name
继承的各个接口使用逗号 , 分隔。
果说我们在一个对象当中,我们某个成员是可有可无的,那对于约束这个对象的接口来说,我们可以使用可选成员这样的一个特性。
interface Post {
title: string
content: string
subtitle?: string // 可选成员
}
初始化过后不能再修改
interface Post {
title: string
content: string
subtitle?: string // 可选成员
readonly summary: string // 只读成员
}
一般用于一些有动态成员的对象,例如程序当中的缓存对象,它在运行当中会出现一些动态的键值。
interface Cache {
// [属性名称(不是固定的,可以是任意名称), 键的类型]:值的类型
[prop: string]: string
}
// 创建一个 cache 对象实现这个 Cache 接口
const cache: Cache = {}
// 动态添加任意的成员,这些成员都必须遵循 Cache 接口的类型约束
cache.foo = 'value1'
cache.bar = 'value2'
TypeScript 类定义方式如下:
class class_name {
// 类作用域
}
作用:描述一类具体事物的抽象特征。
定义类的关键字为 class,后面紧跟类名,类可以包含以下几个模块(类的数据成员):
class Car {
// 字段
engine:string;
// 构造函数
constructor(engine:string) {
this.engine = engine
}
// 方法
disp():void {
console.log("发动机为 : "+this.engine)
}
}
例如:手机就是一个类型,这个类型的特征就是可以用来打电话和发短信,在这个类型下面还有一些细分的子类,而这些子类一定会满足父类的所有特征,而还会多出一些额外的特征。如智能手机,除了可以打电话和发短信,还可以使用一些
app,而我们是不能直接去使用类的,而是使用属于这个类的具体事物,例如我手上的智能手机。类比到程序的角度,类也是一样的,它是用来描述一类具体对象的抽象成员。ES6
以前,都是使用函数+原型 模拟实现类,ES6 开始 JavaScript中 有了专门的 class。而在 TypeScript
当中,除了可以使用 ECMA 标准当中所有类的功能,TypeScript 还增强了 class 的相关语法。
在 ES6 以前,都是通过 函数 + 原型 模拟实现类,从 ES6 开始 JavaScript 中有了专门的 class,在 TypeScript 中 增强了 class 的相关语法。
直接使用 this 去访问当前类的属性会报错,是因为在 TypeScript 当中,我们要明确在类型当中去声明它所拥有的一些属性,而不是直接在构造函数当中通过 this 动态添加。
在 typescript 当中,类的属性必须要有一个初始值,要么在声明的时候通过=号去赋值,要么就在构造函数里初始化。
类的基本写法
class Person {
// 声明这个类所拥有的一些属性
name: string
age: number
constructor (name: string, age: number) {
// 类的属性必须要有一个初始值
// 一般我们都是在构造函数里面去动态设置一个初始值
this.name = name
this.age = age
}
sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
}
}
类中的每一个成员我们都可以使用访问修饰符去修饰。
默认也是 public,如果我们在构造函数前面加一个 private,那么这个类就不能在外部被实例化,也不能被继承,这样的一种情况下我们只能够在类的内部添加一个静态方法,然后在这个静态方法当中创建这个类的实例。
class Person {
public name: string
private age: number
protected gender: boolean
constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
}
}
class Student extends Person {
private constructor (name: string, age: number) {
super(name, age)
}
static create (name: string, age: number) {
return new Student(name, age)
}
}
const jack = Student.create('jack', 18)
如果将构造函数标记为 Protected,也是不能够在外部被实例化,但是相比于 private ,它是运行继承的。
使用 readonly
关键词设置成员只读,初始化过后,readonly
不管在外部还是内部都不允许再修改。
class Person {
public name: string
private age: number
protected readonly gender: boolean
constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
}
}
类与类之间的一些共同点可以用接口去抽象,比如 人 和 动物 都有相同的特点,吃和行走,我们就可以通过接口去约束这两个类的公共能力。
interface EatAndRUn {
eat (food: string) : void
run (distance: number) : void
}
// 通过关键字 implements 实现 这个 EatAndRUn 接口
// 此时这个类必须要有这个接口对应的成员
class Person implements EatAndRUn{
eat (food: string): void {
console.log(`优雅的进餐:${food}`)
}
run (distance: number) {
console.log(`直立行走:${distance}`)
}
}
class Animal implements EatAndRUn{
eat (food: string): void {
console.log(`呼噜呼噜的吃:${food}`)
}
run (distance: number) {
console.log(`爬行:${distance}`)
}
}
需要注意的是:在 C# 和 Java 这些语言当中,它建议我们尽可能让每个接口的定义更加简单更加细化,因此我们建议一个接口只去约束一个能力,让一个类同时实现多个接口。
interface Eat {
eat (food: string): void
}
interface Run {
run (distance: number): void
}
class Person implements Eat, Run {
eat (food: string): void {
console.log(`优雅的进餐: ${food}`)
}
run (distance: number) {
console.log(`直立行走: ${distance}`)
}
}
class Animal implements Eat, Run {
eat (food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
run (distance: number) {
console.log(`爬行: ${distance}`)
}
}
抽象类在某种程度上跟接口有点类似,它也是用来约束子类当中必须要拥有某些成员,但是不同于接口的是,抽象类可以包含一些具体的实现,而接口只能够是一些成员的抽象,不包含具体的实现,一些比较大的类目建议使用抽象类,比如动物类,因为我们所说的动物它只是一个泛指,并不够具体,那在它的下面一定有更细化的分类,例如小狗小猫之类。
定义抽象类的方式:通过关键词 abstract
类型被定义为抽象类过后,它只能够被继承,不能够再使用 new 的方式去创建对应的实例对象,在这种情况下我们就必须要使用 子类 去继承这个类型。
在抽象类当中我们还可以去定义一些抽象方法,需要注意的是抽象方法也不需要方法体。
当我们的父类有抽象方法时,我们的子类就必须要实现这个方法。
abstract class Animal {
eat (food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
abstract run (distance: number): void
}
class Dog extends Animal {
run(distance: number): void {
console.log('四脚爬行', distance)
}
}
const d = new Dog()
d.eat('嗯西马')
d.run(100)
可以使用 vscoe 的代码修正功能,自动去生成所对应的方法现实:
指的是我们去定义函数,接口,类的时候没有去定义具体类型,我们在使用的时候再去定义指定类型的这一种特征。
以函数为例,泛型 就是我们在声明一个函数的时候不去指定一个类型,等我们调用的时候再传入一个具体的类型,这样做的目的是极大程度的复用我们的代码。
比如 定义一个创建 number 类型和 string 类型数组的方法:
// 定义一个创建 number 类型的方法
function createNumberArray (length: number, value: number): number[] {
const arr = Array<number>(length).fill(value)
return arr
}
// 定义一个创建 string 类型的方法
function createStringArray (length: number, value: string): string[] {
const arr = Array<string>(length).fill(value)
return arr
}
const numArr = createNumberArray(3, 100)
const strArr = createNumberArray(3, 'foo')
把类型用一个泛型参数 T 表示,把函数当中不明确类型用 T 去代表,在使用的时候再传递具体类型。如下:
function createArray<T> (length: number, value: T): T[] {
const arr = Array<T>(length).fill(value)
return arr
}
const numArr = createArray<number>(3, 100)
const strArr = createNumberArray<string>(3, 'foo')
其实 Array 是一个泛型类,在 typescript 中去定义这个 Array 类型时,它不知道我们使用它去存放什么样类型的数据,所以它就使用泛型参数,在我们去调用的时候再传入具体类型,这是一个泛型提现。
总的来说,泛型就是在我们定义的时候把不明确的类型,变成一个参数,在我们使用的时候再传递这样的一个类型参数。
在实际开发过程中,难免会使用一些第三方 npm 模块,而这些第三方模块不一定是通过 typescript 编写的,所以它提供的成员就不会有强类型的体验。
import { camelCase } from 'lodash'
declare function camelCase(input: string): string
const res = camelCase('hello typed')
这就是所谓的类型声明,说白了就是一个成员在定义的时候由于某种原因,还没声明一个明确的类型,我们在使用的时候可以单独为它再做一次声明,这种用法存在的原因,为的是考虑兼容一些普通模块的js。
由于 typescript 的社区很强大,绝大部分的常用的 npm 模块都已经做了对应的类型声明,我们只需要安装其所对应的类型声明模块就行了。
比如 lodash 安装其所对应的类型声明模块
add @types/lodash --dev
安装完毕之后我们就可以看到相应的类型提示了:
现在越来越多模块已经在内部集成了这些类型声明的文件,很多时候我们都不需要单独安装类型声明模块了。