# typescript 基础
# 静态类型的理解
当变量的类型确定下来时,变量的类型在后面是无法修改的,还有一点就是该变量可以使用该类型上的所有公共方法和公共属性。
const num: number = 1.2; // 后面无法修改到其他类型
num.toFixed(0); // 可以使用数值类型上的toFixed方法,保留0位小数
2
# 基础和复杂类型
因为 ts 是 js 的超集,所以 js 的类型都存在于 ts 里。基础类型有 null、undefined、number、boolean、string、symbol,也叫简单类型。而复杂类型是由 Object 为代表的,它有一些子类,比如 Array、Function 和自定义的复杂类型。
const num: number = 1; // number
const str: string = "a"; // string
const bool: boolean = false; // boolean
const person: { name: string; age: number } = { name: "Bob", age: 25 }; // 自定义的复杂类型(也是对象类型)
2
3
4
# 类型注解和类型推断
**类型注解(type annotation)**声明变量时,给变量做的类型约束,是为了告诉 ts 和开发者该变量是个什么类型。
**类型推断(type inference)**如果声明的变量不给做类型注解,那么 ts 会根据你赋的值来推断它是个什么类型。
const num: number = 1; // 其中: number就是类型注解
const str = "a"; // 会根据'a'推断出str是一个string类型
2
如果定义的是常量并且使用的是基本类型的值,那就可以不用做类型注解。像上面的 num 就可以省略:const num = 1;
。
在方法中传参就得做类型注解,即使你传的是基本类型,因为 ts 无法推测你的方法应该传什么,可能永远都不会调用所以它推不出来,而方法的会返回值可以通过 return 的类型来推测,所以方法的返回接收值可以不用做类型注解。
# 函数
函数 Function 是基于 Object 的,它主要用于描述一类行为,执行一些特定的代码,可以参考查看 js 里的function 类型。
一般的,函数的入参是要做类型注解的。因为 ts 没有根据去推测函数入参的类型的,它的调用处是多样性的,所以必须给函数入参做严格限制,才能保证不影响到函数内部的一些执行。
而函数的返回值可以不用做类型注解,因为可以依赖于 return 来推测,但是函数没有 return 时又偏偏后期某个时间点给它加上了 return,这会影响到所有的调用点,那最好还是给函数的返回值做注解。具体是有 return 就做具体类型注解,没有 return 需求就使用
void
做类型注解。function demo(data: { x: number; y: number }) { // x,y必须做类型注解 const x = Number(data.x.toFixed(1)); return x + data.y; } const result = demo({ x: 1.2, y: 2 }); // result可以不用做类型注解
1
2
3
4
5
6有个经常犯错的场景,就是函数的参数使用解构语法,很容易把对象解构和类型注解混在一个对象里写。下面这个示例与上面一点里的示例非常像,其实你仔细看后面的类型注解一模一样的,只是前面的 data 换成了解构写法而已。
// 入参千万别只写个{ x: number, y: number }就完事了,前面要加上{ x, y }: function demo({ x, y }: { x: number; y: number }): void { // 没有返回值,函数返回类型注解为void console.log(x + y); } demo({ x: 1.2, y: 2 });
1
2
3
4
5
6还有函数类型本身的一个注解,可以使用
(入参注解) => 返回值注解
,也可以直接使用Function
,不过还是推荐使用 interface 来约束函数类型,下面小节里会讲。const func: (x: number, y: number) => string = (x: number, y: number): string => { return (x + y).toString(); }; const func1: Function = (x: number, y: number): string => { // Function也可以,其实上一种更具约束力 return (x + y).toString(); };
1
2
3
4
5
6
7
总的来说,函数入参的类型注解一定要做并且要区分普通的和解构的,然后函数的返回值也要做类型注解用来约束函数的返回值。
# 数组和元组
我们知道 js 的数组是可以存不同类型的值,数组的长度也是动态改变的,详情可以查看 js 的array 类型。
而 ts 的数组会有限制,可以限定你的数组里的元素只能是哪些类型。ts 还有一个元组,它的限定等级更高,它首先限定数组的元素数量再限定具体位置上是什么类型的元素。
// 数组
const arr1: number[] = [1, 2, 3, 4];
const arr2: Array<number> = [1, 2, 3, 4]; // 数组配合泛型使用
const arr3: (number | string)[] = [1, 2, "3", 4]; // 数组配合联合类型使用
// 其实可以使用数组配合泛型(Array<T>)替代这个复杂写法
const arr4: { name: string; age: number }[] = [
{ name: "Sandy", age: 15 },
{ name: "Marry", age: 18 },
];
// 元组
const arr5: [string, string, number] = ["1", "2", 3];
// 数据简单属性少就可以用元组替代对象数组(Array<T>),因为它约束更强,就是一个位置一个位置约定还是很麻烦的
const arr6: [string, string, number][] = [
["Sandy", "student", 15],
["Marry", "student", 18],
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 接口
在其他语言里接口是给具有类似行为或属性的事物制作一个顶级的模板,用于约束这一类事物,里面有抽象的属性和方法而并不实现它们。
js 没有接口,而 ts 引进了接口。作用和其他语言类似也是为了约束一类事物,但由于 js 的缘故,ts 的接口使用起来更灵活。
# 接口约束对象
接口可以约束普通的对象,跟普通的 class 写法类似,但是它只能书写抽象的属性和方法,也就是说不能为它们进行赋值(不能立马实现)。
interface Idata {
// 接口来约束对象
name: string; // 必填属性
readonly age: number; // 只读属性
email?: string; // 可选属性
}
function doSome(person: Idata): string {
return person.name;
}
function doSome1(person: Idata): string {
return person.name + person.age.toString();
}
const person = { name: "Sandy", age: 15, email: "ss@qq.com", profession: "student" };
doSome(person); // 传的是对象指针,不会做强校验,也就说参数可以多但不能少
doSome1({ name: "Bob", age: 19 }); // 传的是对象字面量,会对它做强校验,不符合就会报错
2
3
4
5
6
7
8
9
10
11
12
13
14
15
除了上面的这些用法,还可以往接口里加函数方法,能对函数方法进行约束,同样的也不能对它进行具体的实现。
interface Idata {
name: string; // 必填属性
readonly age: number; // 只读属性
email?: string; // 可选属性
sayAge(): number; // 方法,返回值必须是规定类型的,不然会报错
}
function doSome(person: Idata): void {
console.log(person.sayAge());
}
const person = {
name: "Sandy",
age: 15,
email: "ss@qq.com",
profession: "student",
sayAge: () => {
return person.age;
}, // 必须同接口里的一样定义相同的返回值
};
doSome(person); // 传的是对象指针person,不会做强校验,也就是参数可以多但是不能少
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 接口的实现与继承
除了上述这种直接用于对象的约束,接口当然也能像传统接口的一样使用类来implements(实现)
interface Idata {
name: string; // 必填属性
readonly age: number; // 只读属性
email?: string; // 可选属性
sayAge(): number; // 方法,返回值必须是规定类型的,不然会报错
}
class Person implements Idata {
name: string; // 必填属性
readonly age: number; // 只读属性
email?: string; // 可选属性
constructor() {} // 构造函数
sayAge = (): number => {
return this.age;
}; // 方法,返回值必须是规定的,不然报错
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
接口也能继承接口
interface Idata {
name: string; // 必填属性
readonly age: number; // 只读属性
email?: string; // 可选属性
sayAge(): number; // 方法,返回值必须是规定类型的,不然会报错
}
interface IPerson extends Idata {
sayName(): string; // 新增方法
}
class Person implements IPerson {
name: string; // 必填属性
readonly age: number; // 只读属性
email?: string; // 可选属性
constructor() {} // 构造函数
sayAge = (): number => {
return this.age;
}; // 方法,返回值必须是规定的,不然报错
sayName = (): string => {
return this.name;
}; // 方法,返回值必须是规定的,不然报错
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 接口约束函数
接口可以约束一个函数对象,函数对象本来就是继承自 Object,只是接口是对函数的入参和出参进行约束的(忽略方法体),而对象是对对象的属性进行约束的(忽略属性值)。但只要记住接口的写法都是类似于 class 的,不管它约束谁。
比如下面这个是函数类型注解的一种改造:
interface Ifunc {
(x: number, y: number): string;
}
const func: Ifunc = (x: number, y: number): string => {
return (x + y).toString();
};
2
3
4
5
6
# 接口约束数组
数组除了可以(number | string)[]
,或者[string, number, string]
进行类型注解,也可以配合接口对数组进行类型注解。
interface Iarr {
[index: number]: string;
}
const arr: Iarr = ["1", "2", "3"];
2
3
4
# 类
es6 之前都是使用原型链来实现继承(甚至要配合构造函数和 call)会比较麻烦,es6 推出了 class 方式的继承,同样的 ts 也能使用 class 方式继承。
class X {
a: string = "a"; // 公共属性(都可访问)
public b: string = "b"; // 公共属性(都可访问)
protected c: string = "c"; // 受保护属性(自己和子类可访问)
private _d: string = "d"; // 私有属性(只有自己可访问)
get d(): string {
return this._d;
} // 通过暴露的方法来间接操作私有属性
set d(d: string) {
this._d = d;
} // 通过暴露的方法来间接操作私有属性
constructor() {} // 构造函数
funcA() {} // 公共方法
public funcB() {} // 公共方法
protected funcC() {} // 受保护方法(自己和子类可访问)
private funcD() {} // 私有方法(只有自己可访问)
}
class Y extends X {
// 继承一个,可以继承到受保护的和公共方法
a: string = "a"; // 重写覆盖
public b: string = "b"; // 重写覆盖
protected c: string = "c"; // 重写覆盖
// private _d: string = 'd'; // 无法重写覆盖,因为它是父类私有的属性
constructor() {
super();
} // 构造函数必须第一行就调用super();
funcA() {} // 重写覆盖
public funcB() {} // 重写覆盖
protected funcC() {} // 重写覆盖
// private funcD() { } // 无法重写覆盖,因为它是父类私有的属性
}
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 Z {
static a: string = "a";
}
const a = Z.a;
2
3
4
静态属性和private修饰符可以搭配使用写一个单例模式
class Demo {
private static _instance: Demo;
consrtuctor() {}
public static instance(): Demo {
return this._instance ? this._instance : new Demo();
}
}
2
3
4
5
6
7
抽象类可以作为其它派生类的基类来使用,和前面的接口很像,但是没有接口的抽象层次高,抽象类可以包含成员的实现细节,那么为了区分抽象类中的抽象方法和普通方法,使用abstract
关键字定义抽象方法(抽象类本身也使用这个关键字)。
abstract class Ademo {
public move() {}
abstract say(); // 待实现的抽象方法
}
2
3
4
# 联合类型和类型保护
在给变量做类型注解时可以使用联合类型,比如let a: string | number
意思是 a 变量可能是字符串类型也可能是数值类型,当然它也可以用于复杂类型,在使用时只能使用联合类型的共有属性。
interface Ia {
name: string;
fun1: () => string;
}
interface Ib {
name: string;
fun2: () => string;
}
(val: Ia | Ib) => {
val.name; // 只能使用name属性,fun1和fun2就使用不了
};
2
3
4
5
6
7
8
9
10
11
解决上面的问题可以使用类型保护,比如其中一种是类型断言:
interface Ia {
name: string;
fun1: () => string;
}
interface Ib {
name: string;
fun2: () => string;
}
(val: Ia | Ib) => {
(val as Ia).fun1; // 断言成Ia类型,然后就可以使用fun1
};
2
3
4
5
6
7
8
9
10
11
还可以使用in
操作符:
interface Ia {
name: string;
fun1: () => string;
}
interface Ib {
name: string;
fun2: () => string;
}
(val: Ia | Ib) => {
if ("fun1" in val) val.fun1;
// 存在这个方法就能使用
else if ("fun2" in val) val.fun2;
};
2
3
4
5
6
7
8
9
10
11
12
13
还可以使用typeof
和instanceof
操作符:
// typeof
(a: number | string, b: number | string) => {
if (typeof a === "string" || typeof b === "string") return `${a}${b}`;
else return a + b;
};
// instanceof(只能用在class,不能用在interface)
class A {
name: string;
fun: () => string;
}
class B {
fun: () => string;
}
(v1: A | B, v2: A | B) => {
if (v1 instanceof A && v2 instanceof A) `${v1.name}${v2.name}`;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 枚举类型
枚举就是一种可以一一列举出它所有值的一种类型,常用于一些判断语句与常量的搭配使用中。
enum Status { // 定义一个枚举类型
START = 0, // 不赋值0也是默认0
PENDDING, // 如果这里赋值6,那么END值就会是7,而START还是0
END,
}
((status: Status) => {
// 入参的类型注解就是枚举类型
if (status === Status.START) {
// 枚举的正向使用
console.log(Status[0]); // 枚举的正向使用
} else if (status === Status.PENDDING) {
console.log(Status[1]);
} else if (status === Status.END) {
console.log(Status[2]);
}
})(0); // 传的值可以是Status.START也可以直接是0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面的例子就是枚举怎么定义和怎么使用。枚举本身是从 0 开始逐渐往后增加,也可以直接给第一项指定初始值,那枚举所有项都是从第一个值往后增加。其实枚举的值除了是数值以外也可以是字符串,但不会有数值自增的性质,必须每项都用字符串(其实数值和字符串混合也可以但不太建议)。
enum Status { // 定义一个枚举类型
START = "START",
PENDDING = "PENDDING",
END = "END",
}
2
3
4
5
# tsconfig.json
tsconfig.json (opens new window)是 ts 的编译配置文件,也包含一些静态检查。
tsc xxx.ts
指定了对单个文件的编译,暂时不会用到 tsconfig.json,会使用默认的配置项;而tsc
不指定文件,就会去根目录里搜索 tsconfig.json,并使用其中的配置项进行编译。
指定文件可以使用"include"和"exclude"配置项,比如只编译主要文件,忽略 node 模块和测试文件:
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
2
3
4
5
6
7
"compilerOptions"是编译选项 (opens new window),常用的:
- target:指定 ECMAScript 目标版本 "ES3"(默认), "ES5", "ES6"/ "ES2015", "ES2016", "ES2017"或 "ESNext"。
- module:指定生成哪个模块系统代码: "None", "CommonJS", "AMD", "System", "UMD", "ES6"或 "ES2015"。
- lib:编译过程中需要引入的库文件的列表。比如值是["es2017", "DOM"],需要引入 es2017 和 dom 模块。
- outDir:重定向输出目录。编译输出目录。
- strict:启用所有严格类型检查选项。当于启用 noImplicitAny、noImplicitThis、alwaysStrict、strictNullChecks、strictFunctionTypes、strictPropertyInitialization。
- noImplicitAny:在表达式和声明上有隐含的 any 类型时报错。
- noImplicitThis:当 this 表达式的值为 any 类型的时候,生成一个错误。
- alwaysStrict:以严格模式解析并为每个源文件生成 "use strict"语句。
- esModuleInterop:通过为所有导入创建命名空间对象,实现 CommonJS 和 ES 模块之间的互操作性。
- noUnusedLocals:若有未使用的局部变量则抛错。
- noUnusedParameters:若有未使用的参数则抛错。
- noImplicitReturns:不是函数的所有返回路径都有返回值时报错。
- removeComments:删除所有注释,除了以 /!*开头的版权信息。
可以考虑使用的:
- rootDir:仅用来控制输出的目录结构 --outDir。
- outFile:把编译输出文件放到一个文件里。
- incremental:是否启用增量编译。
- allowJs:允许编译 javascript 文件。
- checkJs:在 .js 文件中报告错误。与 --allowJs 配合使用。
- sourceMap:调试文件,相关的有 inlineSourceMap 还有 mapRoot。
- baseUrl:根路径。
与 react 有关的可以看一下学习 react 的准备工作