TypeScript入门

本文最后更新于:4 个月前

TypeScript语法、类型声明文件、高级语法……

TypeScript入门

TypeScript官方文档更适合作为参考文档去查阅语法,并不适合新手去学习。想要快速有效地了解TypeScript,推荐阅读以下两个非常不错的文档!

推荐阅读:

什么是 TypeScript · TypeScript 入门教程 (xcatliu.com)

深入理解 TypeScript | 深入理解 TypeScript (jkchao.github.io)

一、TypeScript是什么

  • TypeScript 是添加了类型系统的 JavaScript,适用于任何规模的项目。
  • TypeScript 是一门静态类型、弱类型的语言。
  • TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。
  • TypeScript 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中。
  • TypeScript 拥有很多编译选项,类型检查的严格程度由你决定
  • TypeScript 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript。
  • TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力。
  • TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明。
  • TypeScript 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)。

详情请看:什么是 TypeScript · TypeScript 入门教程 (xcatliu.com)

二、基本类型

这里的基本类型,指在 JavaScript 中也存在的类型

基础类型

1
2
3
4
5
let isDone: boolean = false;	// 布尔值
let decLiteral: number = 6; // number
let name: string = "bob"; // string
let u: undefined = undefined; // undefined
let n: null = null; // null

数组类型,有两种声明方式:

第一种,类型 []

1
let list: number[] = [1, 2, 3];

第二种, Array<elemType>数组泛型的形式

1
let list: Array<number> = [1, 2, 3];

如果数组中存在多种不同类型时,使用 any

1
let list: any[] = [1, true, "free"];

三、特殊类型

这里的特殊类型,指 TypeScript 特有的,在 JavaScript 中不存在的类型

1. 任意值any

如果是一个普通类型,在赋值过程中改变类型是不被允许的:但如果是 any 类型,则允许被赋值为任意类型。

1
2
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

在任意值上可以访问任何属性、调用任何方法,而不会报错(这在其它类型上是不允许的)

1
2
3
let anyThing: any = 'Tom';
console.log(anyThing.myName);
anyThing.setName('Jerry');

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

1
2
3
let something;
// 等价于
let something: any;

如果一个数组中,包含了不同的类型

1
2
// 数组中包含了不同的类型
let list: any[] = [1, true, "free"];

2. 元组Tuple

1
2
3
4
5
// Tuple 元组
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error

3. 枚举enum

使用枚举,可以定义一些常量。被枚举的值只能是以下两种:数字、字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
// 枚举字符串
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}

// 枚举数字
enum Response {
No = 0,
Yes = 1,
}

枚举数字时,具有「自增长」的特点

1
2
3
4
5
6
7
8
9
enum Response {
No = 0,
Yes // 此时Yes等于1
}

enum Response {
No = 1,
Yes // 此时Yes等于2
}

以下是枚举的用法

1
2
3
4
5
6
7
8
9
10
11
12
// 定义枚举
enum Response {
No = 0,
Yes = 1,
}

// 下面是使用
function respond(recipient: string, message: Response): void {
// ...
}
respond("Princess Caroline", Response.Yes) // 可以通过枚举的属性访问
respond("Princess Caroline", Response[0]) // 也可以通过枚举的索引访问

4. never

1
2
3
4
5
6
7
8
9
10
11
// never 常用来表示那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}

四、高级类型

1. 联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种,符号为 |

1
2
3
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法

1
2
3
4
function getLength(something: string | number): number {
return something.length;
}
// 报错
1
2
3
4
function getString(something: string | number): string {
return something.toString();
}
// 没有问题

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

1
2
3
4
5
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

2. 交叉类型

把多个类型合并为一个类型,,符号为 &

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
name: string;
}

interface Student {
grade: number;
}

let obj: Person & Student = {
name: 'LiMing',
grade: 2,
}

3. 索引类型

示例如下

1
2
3
4
5
6
7
8
9
10
function getValue<T extends object, U extends keyof T>(obj: T, key: U): T[U] {
return obj[key];
}

const a = {
name: 'timegogo',
age: 26,
}

getValue(a, 'name');

keyof索引类型查询操作符。 对于任何类型 Tkeyof T的结果为 T上已知的公共属性名的联合。

T[U]索引访问操作符

另外一种实践中很常见的用法:允许额外的类型

1
2
3
interface Map<T> {
[key: string]: T;
}
1
let x: { foo: number, [x: string]: any };

五、接口(对象的类型)

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。接口一般首字母大写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Person {
name: string;
age: number;
}

let tom: Person = {
name: 'Tom'
};
// 报错,变量的属性必须与接口一致
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
// 报错,变量的属性必须与接口一致

后面带个?,表示属性可选

1
2
3
4
interface SquareConfig {
color?: string;
width?: number;
}

前面加一个readonly,表示只读属性

1
2
3
4
interface Point {
readonly x: number;
readonly y: number;
}

额外的属性检查,如果定义对象时传入了额外的(没有定义在接口中的)属性,会报错

1
2
3
4
5
6
7
8
9
10
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; width: number } {
// ...
}

let mySquare = createSquare({ colour: "red", width: 100 });
// 传入colour,会被检测出来,并报错。这对于发现bug很有帮助,因为在一堆代码中肉眼很难看出colour和color之间的区别

六、类型推论 & 断言 & 兼容

1. 类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型

1
2
3
4
5
let myFavoriteNumber = 'seven';
// 实际上等价于let myFavoriteNumber: string = 'seven';
// string是TS自动根据 值 推导出来的

myFavoriteNumber = 7; // 报错,Type 'number' is not assignable to type 'string'.

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

1
2
3
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

2. 类型断言

类型断言好比其它语言里的类型转换,有两种写法:“尖括号” 或者 as

1
2
3
let strLength: number = (<string>someValue).length;		// 尖括号写法

let strLength: number = (someValue as string).length // as语法

注意:在JSX中只能使用as这种断言语法!

双重断言,毫无根据的断言又是会被TS拒绝,并报错。这是可以通过先断言成any或者unknown,再进一步断言成其它类型

1
2
3
function handler(event: Event) {
const element = (event as any) as HTMLElement; // ok
}

3. 类型兼容

原始类型对象类型 的类型兼容,“字段多” 赋值给 “字段少” 是正确的

1
2
3
4
5
6
7
8
interface Named {
name: string;
}

let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y; // 正确,因为 x 需要的字段 y 都有
y = x; // 报错,因为 y 需要的一些字段 x 没有,Property 'location' is missing in type 'Named' but required in type '{ name: string; location: string; }'

函数 的类型兼容,

参数的角度,“参数少的” 赋值给 “参数多的” 是正确的

1
2
3
4
5
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // 正确,因为函数x的参数在函数y中都有,想一想js允许忽略参数的例子,Array.prototype.map回调函数
x = y; // 错误

返回值的角度,”返回值多的“ 赋值给 ”返回值少的“ 是正确的

1
2
3
4
5
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // 正确,x需要返回的东西,在y的返回值里面都有,类比于对象类型的兼容
y = x; // 错误

三、函数类型声明

1. 内联声明

1
2
3
function fn(bar: string, foo: number): boolean {
...
}

2. 接口声明

可以使用:冒号

1
2
3
4
5
6
7
interface SearchFunc {
(source: string, subString: string): boolean;
}
// 等价于
type SearchFunc = {
(source: string, subString: string): boolean;
}

也可以使用=>箭头

1
2
// interface关键字 不能与 =>同时使用。interface接口声明函数只能使用 : 冒号
type SearchFunc = (source: string, subString: string) => boolean;

下面是对函数声明的使用/消费

1
2
3
let mySearch: SearchFunc = function(source, subString){
...
}

3. 函数重载声明

多次声明函数头,实现函数重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
function padding(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a;
} else if (c === undefined && d === undefined) {
c = a;
d = b;
}
return {
top: a,
right: b,
bottom: c,
left: d
};
}

上三行是函数声明,第四行及往下是函数实现。

使用接口进行重载声明

1
2
3
4
5
6
7
8
9
type CompareFunc = {
(left: string, right: string) : boolean;
(left: number, right: number) : boolean;
}
// 等价于
interface CompareFunc {
(left: string, right: string): boolean;
(left: number, right: number): boolean;
}
image-20230927004802083
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Overloaded {
(foo: string): string;
(foo: number): number;
}

// 实现接口的一个例子:
function stringOrNumber(foo: number): number;
function stringOrNumber(foo: string): string;
function stringOrNumber(foo: any): any {
if (typeof foo === 'number') {
return foo * foo;
} else if (typeof foo === 'string') {
return `hello ${foo}`;
}
}

const overloaded: Overloaded = stringOrNumber;

4. 剩余参数声明

1
function buildName(firstName: string, ...restOfName: string[]) {...}

四、泛型

使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据.

比如有一个需求:函数接受、返回同一种类型,但是可以是任意类型,实现如下:

1
2
3
4
function identity<T>(arg: T): T {
return arg;
}
// 这 T 就表示泛型

上面定义了泛型函数,下面有两种方法使用

1
2
3
4
5
// 第一种,在<>里面传入参数
let output = identity<string>("myString")

// 第二种,利用类型推论,根据传入的参数自动确定T的类型
let output = identity("myString")

泛型接口

把泛型参数T,作为接口的一个参数

1
2
3
4
5
6
7
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return tag;
}
let myIdentity: GenericIdentityFn<number> = indentity;

对比一下非泛型

1
2
3
4
5
6
7
8
interface GenericIdentityFn {
arg: number): number;
}
function identity(arg: number){
return arg
}

let myIdentity: GenericIdentityFn = indentity;

泛型约束

假设泛型T,只能传递 string 和 number 两种类型,那么可以使用 <T extends xx>的方式进行约束,示例如下

1
2
3
4
type Params = string | number;
class Stack<T extends Params> {
...
}

六、tsconfig.json

如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录

何时使用

  • 不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。
  • 当命令行上指定了输入文件时,tsconfig.json文件会被忽略

文件结构解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"compilerOptions": { # 编译器选项
"declaration": true, # 生成相应的 .d.ts文件,默认false
"declarationDir": "dist/type", # 指定.d.ts文件的的存放目录,必须在declaration为true的情况下才能设置
"outDir": "dist", # 指定输出目录
"module": "commonjs", # 指定模块系统,可以是 "CommonJS","AMD" 或 "System" 等
"noImplicitAny": true, # 在表达式和声明上有隐含的 any类型时报错,默认值false
"preserveConstEnums": true, # 保留 const和 enum声明,默认false
"removeComments": true, # 删除所有注释,除了以 /!*开头的版权信息,默认值false
"sourceMap": true, # 是否生成sourceMap文件,默认false
"typeRoots": ["./typings"], # 默认所有可见的"@types"包会在编译过程中被包含进来,如果指定,只有root下的会被包含进来
"types": ["node", "lodash", "express"] # 只有被列出来的包才会被包含进来,细节参考官方文档,指定为[]禁止引入@types
},
"files": [ # 指定tsc编译的范围,优先级最高
"core.ts",
...
],
"include": [ # 通过glob匹配语法来指定tsc编译范围,优先级最低
"src/**/*"
],
"exclude": [ # 通过glob匹配语法来指定tsc编译范围,优先级居中
"node_modules",
"**/*.spec.ts"
],
"extends" : "./config/base" # 从另一个配置文件里继承配置
}

exclude可以过滤掉include指定的范围,但是无法过滤掉files指定的范围

注意:每个配置项都具有默认值,并不是所有项目都需要手动配置

如果"files""include"都没有被指定,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts, .d.ts.tsx

七、.d.ts文件

  • d:declare,声明

.d.ts,TypeScript的类型声明文件

*.d.ts文件中的顶级声明必须以declareexport修饰符开头

1. 作用是什么

有时,我们不免会引入外部的 JS库,这时TS就对引入的JS文件里变量的具体类型不明确了,为了告诉TS变量的类型,因此就有了.d.ts

很多主流的库都是 JS编写的,并不支持类型系统。这个时候你不能用TS重写主流的库,这个时候我们只需要编写仅包含类型注释的 d.ts 文件,然后从您的 TS 代码中,可以在仍然使用纯 JS 库的同时,获得静态类型检查的 TS 优势。

以下是一个.ts文件

1
2
3
4
5
6
7
8
9
interface Person {
firstName: string;
lastName: string;
}
function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}
let user = { firstName: "Jane", lastName: "User" };
document.body.innerHTML = greeter(user);

下面是tsc为上面那个文件生成的.d.ts文件

1
2
3
4
5
6
7
8
9
interface Person {
firstName: string;
lastName: string;
}
declare function greeter(person: Person): string;
declare let user: {
firstName: string;
lastName: string;
};

2. @types/*文件

npm包的声明文件

多数情况下,类型声明包的名字总是与它们在`npm`上的包的名字相同,但是有`@types/`前缀

与 tsconfig.json 文件中 compilerOptions 中的 typeRootstypes 搭配使用。

默认情况下所有可见的@types包,会在编译过程中包含进来,例如:./node_modules/@types/../node_modules/@types/../../node_modules/@types/等等。

但是,如果指定了typeRoots或者types,那么只有typeRoots目录下的包才会被引入,或者被types指定的包才会被引入。

设置"types": []会禁用自动引入@types包的功能

3. shims-vue.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* shims-vue.d.ts的作用
* 为了 typescript 做的适配定义文件,因为.vue 文件不是一个常规的文件类型,ts 是不能理解 vue 文件是干嘛的,
* 加这一段是是告诉 ts,vue 文件是这种类型的。
* 可以把这一段删除,会发现 import 的所有 vue 类型的文件都会报错。
*/

declare module '*.vue' { //declare声明宣告, 声明一个ambient module(即:没有内部实现的 module声明)
import Vue from 'vue'
export default Vue
}

declare module 'vue-echarts' // 引入vue-echarts

作者:deepCode
链接:https://juejin.cn/post/7099751721865314318
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

4. shime-jsx.d.ts

八、typescript模块

  • 一般作为项目的开发依赖
  • .ts文件正常情况下不能直接运行,需要先经过编译。但是使用ts-node - npm (npmjs.com)可以直接运行

TypeScript入门
http://timegogo.top/2023/06/23/JavaScript/TypeScript:TypeScript入门/
作者
丘智聪
发布于
2023年6月23日
更新于
2023年10月29日
许可协议