TypeScript基础语法

TypeScript 是 JavaScript 的一个超集,或者说 TS 是基于 JS 的,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发,代码开源于 GitHub 上,它可以编译成纯 JavaScript;编译出来的 JavaScript 可以运行在任何浏览器上(不能直接运行 TS)。
我对其简单的理解就是强类型的 JS,强类型可以给我们带来静态语言的一些好处,比如可读性和可维护性,也更加适合 IDE 做语法分析和代码提示,同时它也有强大的类型推断,缺点就是学习成本和开发成本(挺明显的)。
这样看来 TS 对写后端项目可是极其友好,学 TS 也是只看与 JS 的不同点即可,对于 JS 和 Java 基础不错的人,在很多方面真的都是似曾相识,理解起来简单很多。
现在的前端项目使用 TS 的越来越多,不管怎么样,还是了解一下为好。

安装

需要 Node 的环境,并且 Node 无法直接运行 TS 文件,所以使用下面的命令安装 ts 工具:

1
2
3
4
5
6
npm install typescript -g
# 安装便捷的 node 运行工具
npm install ts-node -g

# 监控文件变化,自动编译
tsc -w

然后就可以使用 tsc、ts-node 这些命令了。

常用类型

静态类型带来的好处不光是强约束,还有代码提示,因为类型固定,所以此类型的方法也都明确知道了,通过代码来说明:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 基础类型
const num: number = 123;
const tag: string = 'tt';
let temp: number | string;
let tempArr: (number | string)[];
// 元组
const tupleArr: [number, string] = [1, 'a'];

class Student {}
// 对象类型,定义也可以独立出去,例如 student
const Person: {
name: string;
age: number;
} = {
name: 'mps',
age: 12,
};

const arr: number[] = [1, 2, 3];
const stu: Student = new Student();

// 函数类型的两种定义
const fun: () => number = () => {
return 2 ** 2;
};
// 可省略返回值类型使用自动推断
const fun2 = () => {
return 2 ** 2;
};

// 自动推断返回值
function getSum(x: number, y: number) {
return x + y;
}
// 手动约束返回值
function getSum2(x: number, y: number): number {
return x + y;
}
// 解构语法赋值约束
function getSum3(
{ x, y }: { x: number; y: number }
): number {
return x + y;
}

像类似 const num: number 的定义叫做类型注解,也就是我们告诉 TS 它是什么类型;如果直接不写类型直接写值,那么会自动推断出类型来,当无法推断的时候,只能使用类型注解的方式了。
特殊的,如果返回值空,使用关键字 void 标注,最好还是标注一下,虽然有自动推断;其他特殊的有 never 表示永远不会执行到最后,例如抛出异常或者无限循环之类。
对于元组类型,可以理解为固定长度,固定顺序的数组,需要你精确控制各个类型的顺序,例如读取 CSV 的时候。

接口

概念不多说,跟静态语言里的基本一致,也是抽取共性:

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
27
28
29
interface Point {
x: number;
y: number;
desc?: string;
alert?(): void;
alert2?: () => void;
readonly name?: string;
// 允许其他类型
[propName: string]: any;
}

const p: Point = {
x: 2,
y: 3,
};

// 接口函数类型
interface echoFun {
(desc: string): string;
}
const str: echoFun = (desc) => {
return desc;
};

// 接口函数『重载』
interface Fun {
(): string;
(param: string): string;
}

接口非常灵活,能用接口就用接口,实在做不了再考虑类型别名之类。
其他的都差不多,都是给 class 实现用的,可以多实现,接口也可以继承。
这里补充一下,当你使用结构赋值直接传递的时候 TS 是强校验,就是说多一个少一个都不行;但是你通过对象引用来传递的时候,就不会那么强了,只要不少就行,可以多;亦或者定义中允许其他类型,例如上面的接口。
在 TS 编译过程中,接口会被消除,也就是接口只是在 TS 开发阶段帮助我们的工具。

TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。
类在面向对象语言中非常常见,用来封装的好工具,继承、重写等概念之类的不多说,熟悉静态语言的都很熟了,在这里也都基本一样。

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
27
28
29
30
31
class Animal {
public desc: string = '';
private _name: string;
static isAnimal(a) {
return a instanceof Animal;
}
constructor(name: string, public tag: string) {
this._name = name;
}
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
}
}

let a = new Animal('Kitty', 'tag'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Tom
console.log(a.tag); // tag
Animal.isAnimal(a); // true

// 单例模式,这里使用普通的延迟加载也可以
class Demo {
private static INSTANCE = new Demo();
private constructor() {}
static getInstance() {
return this.INSTANCE;
}
}

相比 Java,getter 和 setter 方法定义有些许不同,调用的时候也不需要按照方法的形式,不过总体来说基本一致。
静态方法可以直接调用,也可以通过实例调用,这一点上一致;可以定义抽象类,可以使用 public、private 和 protected 访问修饰符,不写的话默认是 public;并且可以通过 readonly 关键字定义只读属性。
在构造方法中通过类似 public/private tag: string 的语法可以快速定义类似上例中 name 的属性,也就是省去了定义和赋值的代码。
在 ES7 中,还加入了实例属性和静态属性,真是越来越熟悉了。

类型别名

在定义比较复杂的类型声明时,可以使用类型别名抽取,常用于联合类型(叠加):

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
type Teacher = {
name: string;
age: number;
}

const t: Teacher= {
name: 'mps',
age: 12,
};

// 联合类型
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;


class Stu {
name: string = 'mps';
clazz: string = 'one';
}
// 联合类型的转换
function fn(data: Stu | Teacher) {
// 第一种方式
if (data instanceof Stu) {
(data as Stu).clazz;
}
// 第二种方式,不需要 as 强转
if ('age' in data) {
data.name;
}
}

const n = '123' as string;

// 类型保护
function add(x: number | string, y: number | string) {
if(typeof x == 'string' || typeof y == 'string') {
return `${x}${y}`;
}
return x + y;
}

别名与接口的一个区别也就是类型别名可以做联合类型,另外例如上面的将 name 设置为 string 的别名,接口就做不到,这也很符合别名这个概念。

枚举和泛型

枚举概念上与 Java 中类似,不过幸好远远没有 Java 中那么复杂;也终于看到了泛型,熟悉 Java 的我使用起来没有什么障碍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Status {
ONE,
TWO = 3,
THREE
}

// 0 3 4
console.log(Status.ONE)
console.log(Status.TWO)
console.log(Status.THREE)
// 反查
console.log(Status[0])


// 泛型
function Gen<E, T>(data: E): E {
return data;
}

// 泛型中的 keyof 用法
function Gen2<E extends keyof Teacher>(data: Teacher, key: E): Teacher[E] {
return data[key];
}

枚举里的项默认就是连续数字,默认从 0 开始,如果你手动赋值,之前的从 0 开始,之后就按你赋值的数依次加一;当然你也可以手动设置为字符串类型。
TS 中的泛型类型推断要强得多,很多情况虽然可以不写,但是建议写上清晰一点;也可以使用 extends 和 supper 关键字,甚至可以这样写:T extends number | string,灵活性是更多一些。
尤其注意 TS 中泛型的 keyof 用法,这相对来说是个新语法,就是展开对象的 key。

装饰器

使用之前需要在配置文件中开启 experimentalDecorators,这是 ES7 的东西,好用是好用。
首先,装饰器本身也是一个函数,毕竟 JS 中函数是一等公民;装饰器的参数是一个(作用在)构造函数,通过 @ 来调用,示例:

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
27
28
29
30
31
32
33
34
35
36
// new 表示是构造函数,可简单理解为传入一个有构造的对象(类)返回一个装饰后的类
function testDecorator<T extends new (...args: any[]) => any>(constructor: T) {
return class extends constructor {
echo() {
console.log('decorator');
}
};
}
function testDecorator2(flag: boolean) {
if (flag) {
return testDecorator;
}
return () => {};
}

function testDecorator3() {
return testDecorator;
}

@testDecorator
@testDecorator2(false)
class Test {
constructor(private name: string) {}
}

// 使用函数的方式装饰,避免调用不到装饰新增的方法
const Test2 = testDecorator3()(
class {
constructor(private name: string) {}
}
);

const tt = new Test('mps');

const tt2 = new Test2('mps');
tt2.echo();

执行时机为类『加载』,同时只会执行一次,无论你 new 没 new 都会执行;一个类可以使用多个装饰器,顺序按照定义的顺序。
上面介绍的是类装饰器,同样的,也有方法装饰器,作用在类的 prototype(如果是静态方法就是构造函数):

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 方法装饰器,target 表示类的 prototype,静态方法为构造函数
function echoDecorator(target: any, key: string, desc: PropertyDescriptor) {
// 禁止修改方法,通过 PropertyDescriptor 控制方法行为
desc.writable = false;
console.log(target, key);

// 异常处理
const fn = desc.value;
desc.value = function () {
try {
fn();
} catch {
console.log('error...');
}
};
}

class MethodDemo {
constructor(private name: string) {}
@echoDecorator
echo() {
console.log(this.name);
}
}

// 方法参数装饰器
function paramDecorator(target: any, key: string, index: number) {
console.log(target, key, index);
}

// @catchDecorator('msg')
function catchDecorator(msg: string) {
return (target: any, key: string, desc: PropertyDescriptor) => {
// 异常处理
const fn = desc.value;
desc.value = function () {
try {
fn();
} catch {
console.log(msg);
}
};
};
}

对于方法装饰器的调用时机,跟类一样,在定义的时候就完成了,不需要等到实例化的时候。
特别的,在 setter 和 getter 方法上,只允许一个有装饰器。
属性的装饰器跟方法装饰器基本一致,只是不再有 PropertyDescriptor 这个参数了,在装饰器里修改也是改的原型上的值,并不能修改实例的值。
而参数装饰器就是多个一个参数位置的参数,其他的基本一致, 我感觉跟注解挺像的。

命名空间

假设在 Web 中运用 TS 编译后的文件,如果就最基本的写法,很多类、函数之类的都会变成全局变量,这肯定是混乱糟糕的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 命名空间
namespace Tese {
export class Demo {
tag: string = '';
}

// 子命名空间
export namespace Sub {
// ...
}
}

///<reference path='' />
namespace Main {
export function main(){
return new Tese.Demo();
}
}

如果不使用 export 导出,默认是调用不到的,当命名空间互相引用的时候,建议是写清楚注释,虽然这并不是必须的。
但是可以看出这种方式并不优雅,为了可读性,建议使用 ES6 的模块化语法,这样就不需要使用 namespace 关键字了。
在 Web 场景下使用 ES6 的模块化打包为一个 JS 文件,可能需要使用 amd 标准,但是浏览器不支持,有需要引入其他的支持库,语法也很繁琐,正是如此 Webpack、parcel 之类的打包工具才崛起了。
在不同的文件中如果存在相同的命名空间,那么 TS 会做融合处理,也就是取并集。

附:配置文件

使用 tsc --init 会在当前目录生成 TS 的编译配置文件,当运行 tsc 的时候自动读取,示例:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
{
"compilerOptions": {
/* Basic Options */
"target": "es5" /* target用于指定编译之后的版本目标: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* 用来指定要使用的模块标准: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": ["es6", "dom"] /* lib用于指定要包含在编译中的库文件 */,
"allowJs": true, /* allowJs设置的值为true或false,用来指定是否允许编译js文件,默认是false,即不编译js文件 */
"checkJs": true, /* checkJs的值为true或false,用来指定是否检查和报告js文件中的错误,默认是false */
"jsx": "preserve", /* 指定jsx代码用于的开发环境: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* declaration的值为true或false,用来指定是否在编译的时候生成相应的".d.ts"声明文件。如果设为true,编译每个ts文件之后会生成一个js文件和一个声明文件。但是declaration和allowJs不能同时设为true */
"declarationMap": true, /* 值为true或false,指定是否为声明文件.d.ts生成map文件 */
"sourceMap": true, /* sourceMap的值为true或false,用来指定编译时是否生成.map文件 */
"outFile": "./", /* outFile用于指定将输出文件合并为一个文件,它的值为一个文件路径名。比如设置为"./dist/main.js",则输出的文件为一个main.js文件。但是要注意,只有设置module的值为amd和system模块时才支持这个配置 */
"outDir": "./", /* outDir用来指定输出文件夹,值为一个文件夹路径字符串,输出的文件都将放置在这个文件夹 */
"rootDir": "./", /* 用来指定编译文件的根目录,编译器会在根目录查找入口文件,如果编译器发现以rootDir的值作为根目录查找入口文件并不会把所有文件加载进去的话会报错,但是不会停止编译 */
"composite": true, /* 是否编译构建引用项目 */
"incremental": true, /* Enable incremental compilation */
"tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true, /* removeComments的值为true或false,用于指定是否将编译后的文件中的注释删掉,设为true的话即删掉注释,默认为false */
"noEmit": true, /* 不生成编译文件,这个一般比较少用 */
"importHelpers": true, /* importHelpers的值为true或false,指定是否引入tslib里的辅助工具函数,默认为false */
"downlevelIteration": true, /* 当target为'ES5' or 'ES3'时,为'for-of', spread, and destructuring'中的迭代器提供完全支持 */
"isolatedModules": true, /* isolatedModules的值为true或false,指定是否将每个文件作为单独的模块,默认为true,它不可以和declaration同时设定 */

/* Strict Type-Checking Options */
"strict": true /* strict的值为true或false,用于指定是否启动所有类型检查,如果设为true则会同时开启下面这几个严格类型检查,默认为false */,
"noImplicitAny": true, /* noImplicitAny的值为true或false,如果我们没有为一些值设置明确的类型,编译器会默认认为这个值为any,如果noImplicitAny的值为true的话。则没有明确的类型会报错。默认值为false */
"strictNullChecks": true, /* strictNullChecks为true时,null和undefined值不能赋给非这两种类型的值,别的类型也不能赋给他们,除了any类型。还有个例外就是undefined可以赋值给void类型 */
"strictFunctionTypes": true, /* strictFunctionTypes的值为true或false,用于指定是否使用函数参数双向协变检查 */
"strictBindCallApply": true, /* 设为true后会对bind、call和apply绑定的方法的参数的检测是严格检测的 */
"strictPropertyInitialization": true, /* 设为true后会检查类的非undefined属性是否已经在构造函数里初始化,如果要开启这项,需要同时开启strictNullChecks,默认为false */
"noImplicitThis": true, /* 当this表达式的值为any类型的时候,生成一个错误 */
"alwaysStrict": true, /* alwaysStrict的值为true或false,指定始终以严格模式检查每个模块,并且在编译之后的js文件中加入"use strict"字符串,用来告诉浏览器该js为严格模式 */

/* Additional Checks */
"noUnusedLocals": true, /* 用于检查是否有定义了但是没有使用的变量,对于这一点的检测,使用eslint可以在你书写代码的时候做提示,你可以配合使用。它的默认值为false */
"noUnusedParameters": true, /* 用于检查是否有在函数体中没有使用的参数,这个也可以配合eslint来做检查,默认为false */
"noImplicitReturns": true, /* 用于检查函数是否有返回值,设为true后,如果函数没有返回值则会提示,默认为false */
"noFallthroughCasesInSwitch": true, /* 用于检查switch中是否有case没有使用break跳出switch,默认为false */

/* Module Resolution Options */
"moduleResolution": "node", /* 用于选择模块解析策略,有'node'和'classic'两种类型' */
"baseUrl": "./", /* baseUrl用于设置解析非相对模块名称的基本目录,相对模块不会受baseUrl的影响 */
"paths": {}, /* 用于设置模块名称到基于baseUrl的路径映射 */
"rootDirs": [], /* rootDirs可以指定一个路径列表,在构建时编译器会将这个路径列表中的路径的内容都放到一个文件夹中 */
"typeRoots": [], /* typeRoots用来指定声明文件或文件夹的路径列表,如果指定了此项,则只有在这里列出的声明文件才会被加载 */
"types": [], /* types用来指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载进来 */
"allowSyntheticDefaultImports": true, /* 用来指定允许从没有默认导出的模块中默认导入 */
"esModuleInterop": true /* 通过为导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性 */,
"preserveSymlinks": true, /* 不把符号链接解析为其真实路径,具体可以了解下webpack和nodejs的symlink相关知识 */

/* Source Map Options */
"sourceRoot": "", /* sourceRoot用于指定调试器应该找到TypeScript文件而不是源文件位置,这个值会被写进.map文件里 */
"mapRoot": "", /* mapRoot用于指定调试器找到映射文件而非生成文件的位置,指定map文件的根路径,该选项会影响.map文件中的sources属性 */
"inlineSourceMap": true, /* 指定是否将map文件的内容和js文件编译在同一个js文件中,如果设为true,则map的内容会以//# sourceMappingURL=然后拼接base64字符串的形式插入在js文件底部 */
"inlineSources": true, /* 用于指定是否进一步将.ts文件的内容也包含到输入文件中 */

/* Experimental Options */
"experimentalDecorators": true /* 用于指定是否启用实验性的装饰器特性 */
"emitDecoratorMetadata": true, /* 用于指定是否为装饰器提供元数据支持,关于元数据,也是ES6的新标准,可以通过Reflect提供的静态方法获取元数据,如果需要使用Reflect的一些方法,需要引入ES2015.Reflect这个库 */
}
"files": [], // files可以配置一个数组列表,里面包含指定文件的相对或绝对路径,编译器在编译的时候只会编译包含在files中列出的文件,如果不指定,则取决于有没有设置include选项,如果没有include选项,则默认会编译根目录以及所有子目录中的文件。这里列出的路径必须是指定文件,而不是某个文件夹,而且不能使用* ? **/ 等通配符
"include": [], // include也可以指定要编译的路径列表,但是和files的区别在于,这里的路径可以是文件夹,也可以是文件,可以使用相对和绝对路径,而且可以使用通配符,比如"./src"即表示要编译src文件夹下的所有文件以及子文件夹的文件
"exclude": [], // exclude表示要排除的、不编译的文件,它也可以指定一个列表,规则和include一样,可以是文件或文件夹,可以是相对路径或绝对路径,可以使用通配符
"extends": "", // extends可以通过指定一个其他的tsconfig.json文件路径,来继承这个配置文件里的配置,继承来的文件的配置会覆盖当前文件定义的配置。TS在3.2版本开始,支持继承一个来自Node.js包的tsconfig.json配置文件
"compileOnSave": true, // compileOnSave的值是true或false,如果设为true,在我们编辑了项目中的文件保存的时候,编辑器会根据tsconfig.json中的配置重新生成文件,不过这个要编辑器支持
"references": [], // 一个对象数组,指定要引用的项目
}

在官方的文档中,各个配置项有详细的说明。如果你使用 ts-node 工具,也会使用这个编译配置文件的。

其他

我们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀。

在 TS 中不能直接 import JS 库,如果需要,要额外安装对应的翻译库,这个 VSC 之类的 IDE 都有提示,会转换到一个 .d.ts 中间文件;这个文件仅仅是用 declare var 语法将对应库的 JS 语法合理化,也就是为了避免 IDE 的错误提示,即使不用也可以使用 parcel 之类正确的打包。

相比 Webpack,parcel 更快,也无需配置即可使用,做 demo 是很适合用的。

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~