- 变量声明后,它的类型就确定了。
- 定义变量时,如果没有明确的指定类型,那么 ts 会依照类型推论(Type Inference)的规则推断出一个类型
- 定义变量时,如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查
let a = 1 // 等效于 let a: number = 1
a = '1' // error
let b // 等效于 let b: any
b = 1 // ok
b = '1' // ok
b.split(' ') // ok
let a = 1
,声明变量的同时进行了初始化操作,ts 知道 1 是 number 类型,因此 ts 能够推导出变量 a 的类型是 number。
let b
,仅仅声明了变量 b,但是并没有进行初始化操作,ts 不知道变量 b 是什么类型,ts 会将其视作 any 类型。
- ts 对于函数的约束,主要体现在:参数、返回值
- 调用函数时,输入多余的(或者少于要求的)参数,是不被允许的
function sum1(x: number, y: number): number {
return x + y;
}
sum1(1, 2, 3);
// error 应有 2 个参数,但获得 3 个。
function sum2(x: number, y: number): number {
return x + y;
}
sum2(1);
// error 应有 2 个参数,但获得 1 个。
=>
- 在 ts 的类型定义中,
**=>**
可以用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型 - 在 ES6 中
=>
是定义箭头函数使用的语法
- 在 ts 的类型定义中,
let sum1 = function (x: number, y: number): number {
return x + y;
};
let sum2: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
interface ISum {
(x: number, y: number): number;
}
let sum3: ISum = function (x: number, y: number): number {
return x + y;
};
function sum4(x: number, y: number): number {
return x + y;
}
function sum5(x: number, y: number) {
return x + y;
}
let sum6 = (x: number, y: number): number => x + y
// ……
约束函数的写法是非常灵活的,上述每一个 sum 函数的声明,描述的类型约束信息都是一致的 (x: number, y: number) =>number
- 函数的可选参数、默认参数
- 语法
**?:**
- 可选参数必须接在必填参数后面
- ts 会将带有默认值的参数视作可选的
- 默认参数不必位于必填参数之后
- 语法
// ok
let sum1 = (x: number, y?: number) => {
if (y) {
return x + y;
} else {
return x;
}
}
// error 必选参数不能位于可选参数后。
let sum2 = (x?: number, y: number): number => {
// ...
}
// ok
let sum3 = (x: number, y: number = 0) => x + y
// ok
let sum4 = (x: number = 0, y: number) => x + y
- 函数重载
- 重载允许一个函数接受不同数量或类型的参数时,作出不同的处理
- 函数重载可以约束函数的调用方式
// 约束函数被调用的方式 1
function combine(input1: number, input2: number): number;
// 约束函数被调用的方式 2
function combine(input1: string, input2: string): string;
function combine(input1: any, input2: any) {
if (typeof input1 === "number" && typeof input2 === "number") {
return input1 + input2;
} else if (typeof input1 === "string" && typeof input2 === "string") {
return input1.concat(input2);
}
}
// ok
combine(1, 2) // 3
// ok
combine('1', '2') // 12
// error 没有与此调用匹配的重载。
combine(1, '2')
instanceof
- instanceof 是一个二元操作符
- instanceof 用于测试构造函数的 prototype 属性是否出现在对象的原型链中的任何位置
- 判断对象 a 是否由构造函数 A 创建
a instanceof A
- 判断对象 a 是否由类 A 创建
a instanceof A
- 在 ts 和 js 中,类是特殊的构造函数
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // 在这里,ts 知道 animal 是 Dog 类型
} else if (animal instanceof Cat) {
animal.meow(); // 在这里,ts 知道 animal 是 Cat 类型
}
}
const myDog = new Dog();
const myCat = new Cat();
makeSound(myDog); // 输出: Woof!
makeSound(myCat); // 输出: Meow!
- 声明文件必需以
.d.ts
为后缀。 - 我们通常不会为
.ts
文件手动编写类型声明文件,因为.ts
文件本身就包含了类型信息。 .d.ts
文件主要用于为纯 js 代码提供类型信息。- 类型声明
.d.ts
文件对ts
说:请相信我,按照我说的来做,我会告诉你哪些变量是存在的,以及这些变量的类型信息,即便实际运行时这东西不存在,或者类型信息和我描述的不符,那也不是你 ts 的锅,都是编写这个类型声明文件的小老弟没搞清楚类型导致的问题。 - 声明文件的常见用途:给那些使用 js 编写的第三方库提供类型声明信息,以获得对应的代码补全、接口提示等功能,让这些 js 库能够更好地被我们的
**.ts**
文件所使用,从而提高代码的质量和开发的效率。 - 类型声明
**.d.ts**
文件仅仅为**ts**
提供类型描述服务,并不会出现在最终的编译结果中。因此,我们只需要在类型声明文件中编写类型信息即可,不要多此一举地在类型声明文件中编写相关实现,这种做法是不被允许的。 - 声明文件相关语法
declare var
declare let
declare const
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface
和type
声明全局类型export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export = commonjs
导出模块export as namespace
UMD 库声明全局变量declare global
扩展全局变量declare module
扩展模块/// <reference />
三斜线指令
- 声明语句中只能定义类型,切勿在声明语句中定义具体的实现。
- 声明全局变量
- 可以使用
declare var
declare let
declare const
来声明全局变量。 declare var
和declare let
这两种写法可以认为是等效的。- 使用
declare var
和declare let
声明的变量的值可能会被更改。 - 使用
declare const
声明的变量的值被认为是不可变的。 - 在实际应用中,尽可能使用
declare let
或declare const
,因为它们与 ES6 和更现代的 js 特性更为一致。
- 可以使用
declare var globalVar: string
declare let globalLet: number
declare const GLOBAL_CONST: boolean
// ok
globalVar = "Hello"
// ok
globalLet = 42
// error 无法分配到 "GLOBAL_CONST" ,因为它是常数。
GLOBAL_CONST = true
当你在某个类型声明文件 .d.ts
中声明了 declare var globalVar: string
之后,这就意味着告诉 ts 在全局环境下,存在一个名为 globalVar
的变量,如果这时候你再去定义 globalVar
变量,ts 就会报错,并告诉你这个变量已经被定义过了,无需重复定义。即便实际上并没有定义过 globalVar
变量,但是 ts 还是会按照你在类型声明文件中的类型信息来判断。
declare function
用于声明一个函数
declare function add(x: number, y: number): number
// ok
add(10, 20)
// error
add(10)
// error
add(10, '20')
declare function fetchData(
options: { url: string },
callback: (data: string) => void
): void;
// ok
fetchData({ url: 'http://example.com' }, (data) => {
console.log(data);
});
declare class
声明全局类
declare class Person {
name: string;
age: number;
constructor(name: string, age: number);
sayHello(): void;
}
// ok
let bob = new Person("Bob", 30);
bob.sayHello();
declare enum
声明枚举类型
declare enum Directions {
Up,
Down,
Left,
Right,
}
declare enum Color {
RED = 0,
GREEN = 1,
BLUE = 2,
}
// ok
let directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right,
];
// ok
function paintWall(color: Color = Color.BLUE) {
// ...
}
// ok
paintWall(Color.RED);
namespace
namespace
是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。- 由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用
module
关键字表示内部模块。但由于后来 ES6 也使用了module
关键字,ts 为了兼容 ES6,使用namespace
替代了自己的module
,更名为命名空间。 - 随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的
namespace
,而推荐使用 ES6 的模块化方案了,故我们不再需要学习namespace
的使用了。 - ts 的
namespace
被淘汰了,但是在声明文件中,declare namespace
还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。
declare namespace
声明命名空间- 用来表示全局变量是一个对象,包含很多子属性
- 命名空间中如果出现只有一个属性的情况,可以采用缩写的形式
- 命名空间允许嵌套
- 多个同名的
namespace
声明会被自动合并成一个命名空间。这种特性叫做“声明合并”(Declaration Merging)
declare namespace A {
var a1: number // 声明可变属性 a1
let a2: number // 声明可变属性 a2
const a3: string // 声明只读属性 a3
enum e {
e1
}
function f1(): void // 声明方法 f1
}
// ok
A.f1();
// ok
A.a1 = 1
// ok
A.a2 = 2
// error 无法为“a3”赋值,因为它是只读属性。
A.a3 = '3'
// ok
A.e.e1
declare namespace A {
function f1(): void;
namespace subA {
function f2(): void;
}
}
// ok
A.f1();
// ok
A.subA.f2();
declare namespace A.subA {
function f2(): void
}
// 等效
// declare namespace A {
// namespace subA {
// function f2(): void
// }
// }
// ok
A.subA.f2();
declare namespace A {
function f1(): void;
namespace subA {
function f2(): void;
}
}
// 等效
declare namespace A {
function f1(): void;
}
declare namespace A {
namespace subA {
function f2(): void
}
}
命名空间可以拆开写,ts 在处理时,会将多个同名的命名空间合并成一个命名空间(即便这些同名的命名空间不存在于同一个类型声明文件中)。这种特性叫做“声明合并”(Declaration Merging)。所以,无论你是在一个 namespace
声明中嵌套子命名空间,还是分开写多个同名的 namespace
声明,最终的效果都是相同的。
至于合并时若出现冲突,ts 会如何处理,暂且不做介绍。如果真的出现冲突,你可能优先需要考虑的是如何修改代码避免冲突,而非思考 ts 合并类型声明文件时对于冲突的处理机制是什么。处理冲突的最简单方法就是改名儿,为其中一个命名空间、函数、变量等提供一个不同的名称即可。
- 在声明文件中使用
interface
、type
interface Ix {
a: "1" | "2";
b: {
c?: string;
};
}
type x = {
a: "1" | "2";
b: {
c?: string;
};
};
// ok
let x1: Ix = {
a: "1",
b: {},
};
// ok
let x2: x = {
a: "1",
b: {},
};
// ok
let x3: Ix = {
a: "1",
b: {
c: "c",
},
};
- 暴露在最外层的
interface
或type
会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到namespace
下
declare namespace MyNamespace {
interface Ix {
a: "1" | "2";
b: {
c?: string;
};
}
type x = {
a: "1" | "2";
b: {
c?: string;
};
};
}
// ok
let x1: MyNamespace.Ix = {
a: "1",
b: {},
};
// ok
let x2: MyNamespace.x = {
a: "1",
b: {},
};
// ok
let x3: MyNamespace.Ix = {
a: "1",
b: {
c: "c",
},
};
- 当我们在 ts 中,通过
npm install foo
下载一个 npm 包 foo,并通过import foo from 'foo'
引入 foo 包,若我们在使用 foo 包时,想要获取到 foo 包的相关类型声明信息,以便享有更友好地智能提示,此时可能就需要我们手写声明文件了。不过,在我们尝试给一个 npm 包创建声明文件之前,首先需要看看它的声明文件是否已经存在。一般来说,npm 包的声明文件可能存在于两个地方:- 与该
npm
包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。 - 发布到
@types
里。我们只需要尝试安装一下对应的 @types 包就知道是否存在该声明文件,安装命令是npm install @types/foo --save-dev
。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。
- 与该
- 假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 import 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:
- 创建一个
node_modules/@types/foo/index.d.ts
文件,存放 foo 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。 - 创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到
types/foo/index.d.ts
中。这种方式需要配置下 tsconfig.json 中的paths
和baseUrl
字段。
- 创建一个
目录结构:
/path/to/project
├── src
| └── index.ts
├── types
| └── foo
| └── index.d.ts
└── tsconfig.json
tsconfig.json 内容:
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
如此配置之后,使用 import foo from "foo"
导入 foo 时,也会去 types 目录下寻找对应的模块声明文件了。
"baseUrl": "./"
baseUrl
用于解析非相对模块名称的基目录。"./"
将基路径设置为当前目录,即tsconfig.json
所在的目录。- 当你在 ts 中,使用
import foo from "foo"
这种非相对路径的方式来引入foo
模块时,ts 会根据 baseUrl 配置的基路径去查foo
。比如在这个配置中,ts 就会基于tsconfig.json
所在的位置去查找foo
。
"paths": { "*": ["types/*"] }
paths
用于设置模块名到文件路径的映射,这是 ts 的一个高级特性,允许你为模块指定特定的路径或别名。"*"
表示匹配任何模块名。意味着当 ts 解析任何模块时,它都会考虑这个路径配置。["types/*"]
对于任何给定的模块名,ts 将尝试在 types 目录下查找与模块名相匹配的文件。例如,如果有一个模块名为 “myModule”,那么 ts 将会在 types/myModule.ts 中查找它(或 .tsx, .d.ts,取决于其他配置和文件的存在情况)。
- npm 包的声明文件主要有以下几种语法:
export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块
- npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用
declare
不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用export
导出,然后在使用方import
导入后,才会应用到这些类型声明。
// ./types/foo/index.d.ts
export const name: string;
export function getName(): string;
export class Animal {
constructor(name: string);
sayHi(): string;
}
export enum Directions {
Up,
Down,
Left,
Right,
}
export interface Options {
data: any;
}
// 等效
// declare const name: string;
// declare function getName(): string;
// declare class Animal {
// constructor(name: string);
// sayHi(): string;
// }
// declare enum Directions {
// Up,
// Down,
// Left,
// Right,
// }
// interface Options {
// data: any;
// }
// export { name, getName, Animal, Directions, Options };
// ./src/index.ts
import { name, getName, Animal, Directions, Options } from "foo";
console.log(name);
let myName = getName();
let cat = new Animal("Tom");
let directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right,
];
let options: Options = {
data: {
name: "foo",
},
};
- 与
declare namespace
类似,export namespace
用来导出一个拥有子属性的对象
// ./types/foo/index.d.ts
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}
// ./src/index.ts
import { foo } from "foo";
console.log(foo.name);
foo.bar.baz();
- 在 ES6 模块系统中,使用
export default
可以导出一个默认值,使用方可以用import foo from 'foo'
来导入这个默认值。 - 只有
function
、class
和interface
可以直接默认导出,其他的变量需要先定义出来,再默认导出
// ./types/foo/index.d.ts
export default function foo(): string;
// ./src/index.ts
import foo from 'foo';
foo();
export default enum Directions {
// ERROR: Expression expected.
Up,
Down,
Left,
Right
}
declare enum Directions {
Up,
Down,
Left,
Right,
}
export default Directions;
针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面:
export default Directions;
declare enum Directions {
Up,
Down,
Left,
Right,
}
- 在 ts 中,针对 commonjs 模块导出,有多种方式可以导入
- 同样使用 commonjs 规范导入
const ... = require('...')
- 使用 es module 规范导入
import ... from '...'
- 使用 ts 特有的写法导入
import ... = require('...')
- 同样使用 commonjs 规范导入
// 导出 commonjs
module.exports = foo; // 整体导出
exports.bar = bar; // 单个导出
// 导入 commonjs
const foo = require('foo'); // 整体导入
const bar = require('foo').bar; // 单个导入
// 导入 es module
import * as foo from 'foo'; // 整体导入
import { bar } from 'foo'; // 单个导入
// ts 特有的写法
import foo = require('foo'); // 整体导入
import bar = require('foo').bar; // 单个导入
export =
- 使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到
export =
这种语法了 - 准确地讲,
export =
不仅可以用在声明文件中,也可以用在普通的 ts 文件中。 - 由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到
export =
这种语法了。但是还是需要再强调下,相比与export =
,我们更推荐使用 ES6 标准的export default
和export
。
- 使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到
// types/foo/index.d.ts
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
需要注意的是,上例中使用了 export = 之后,就不能再单个导出 export { bar } 了。所以我们通过声明合并,使用 declare namespace foo 来将 bar 合并到 foo 里。
实际上,
import ... require
和export =
都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看官方文档。
export as namespace
- 既可以通过