早该学学了.
之前写过Python的类型系统,如果对于写C++,Java,C#等这类语言来说,typing根本不成问题,所以理解TypeScript也不是问题.
特殊的类型
any,unknown与never
any,unknown是”顶层类型”,never是”底层类型”.never类型是所有类型共有的,any类型基本没有限制,unknown类型不能直接调用并且运算是有限的,只能进行比较运算.推荐使用unknown代替any然后使用as转换类型.
类型系统
String与string,Number与number
String与string是不同的,前者是可以包含后者的.但是在ts中,很多方法只能使用后者.
所以推荐只使用后者.
1
2
3
4let obj: Object;
let obj2:{};
obj = { name: "John" };
obj = true;
此外Object类型包括除了undefined和null的基本类型.所以这并不符合直觉,推荐使用object
1
2
3let obj3:object;
obj3 = {name:"John"};
obj3 = 13; //报错 不能将number分配给类型object
object类型包含对象,数组,函数.1
2
3
4
5
6const ccx = { foo: 1 };
ccx.foo = 2;
let t = { foo: 1 };
t.foo = 3;
let hh:object = {foo:1}
// hh.foo 报错 类型不对
此外undefined和null也可以赋值为number,object等等.
TypeScript中单个值也是类型成为值类型1
2
3let t: "dfasdf";
const xy = "https";
console.log(xy);
将多个类型组合起来就是联合类型,如果严格检查也就是设置strictNullChecks
,使得其他类型变量不能被赋值为undefined或null.这个时候就可以用联合类型1
2
3
4
5
6
7
8
9let setting: true | false;
let gender: "male" | "female";
let rainbowColor: "赤" | "橙" | "黄" | "绿" | "青" | "蓝" | "紫";
let name: string | null;
name = "John";
name = null;
对象的合成可以给对象添加新的属性,属于交叉类型.1
let obj5: { foo: string } & { bar: number };
类型别名
1 | type Age = number; |
跟Python的typing和Go语言类似.
数组 元组
1 | let arr: number[] = []; |
const数组中的元素是可以改变的,所以在ts中增加了readonly
,readonly数组是原本数组的子类型.1
2
3const arr5: number[] = [0, 1];
arr5[0] = 3;
let arr6: readonly number[] = arr5;
声明readonly数组1
2
3
4let aa: readonly number[] = [1, 2, 3];
let a1: ReadonlyArray<number> = [1, 2, 3];
let a2: Readonly<number[]> = [];
let a3 = [] as const;
TypeScript 推断类型时,遇到
const
命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
const
命令声明的变量,如果赋值为对象,并不会推断为值类型,这是因为 JavaScript 里面,const
变量赋值为对象时,属性值是可以改变的(数组等同理)
元组tuple
1 | const s: [string, string, boolean] = ["a", "b", true]; |
使用元组时必须声明类型不然会默认数组.1
let ot: [number, string?] | undefined = [1];
使用扩展运算符可以不下成员数量的元组.
元组也有只读元组1
2let readonlyTuple: readonly [number] = [1];
let point = [3, 4] as const;
symbol类型
symbol主要用于类的属性.
ts增加了unique symbol作为symbol的子类型.1
2
3
4
5
6
7
8
9
10
11
12// 正确
const x: unique symbol = Symbol();
// 报错
let y: unique symbol = Symbol();
const x: unique symbol = Symbol();
// 等同于
const x = Symbol();
const a: unique symbol = Symbol();
const b: typeof a = a; // 正确
感觉平常可能用不上…
函数 对象 interface
1 | function hello(txt: string): void { |
函数声明与函数变量声明.前者需要声明参数类型,否则默认为any.后者可以在选择在赋值时写出类型或者在声明变量时添加类型.此外还有这种写法1
2
3
4
5
6
7let add: {
(x: number, y: number): number;
};
add = function (x, y) {
return x + y;
};
箭头函数
1 | const repeat = (str: string, times: number): string => str.repeat(times); |
另外使用?表示可选参数1
2
3
4
5
6function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
默认值也类似.1
2
3
4
5function createPoint(x: number = 0, y: number = 0): [number, number] {
return [x, y];
}
createPoint(); // [0, 0]
rest参数也可以用于将多个值包裹为数组或元组1
2
3
4
5
6
7
8
9
10
11
12function joinNum(...nums: [...number[]]): string {
console.log(nums);
return nums.join(" ");
}
joinNum(1, 2, 3, 4, 5);
function joinNumAndString(...args: [string, number]) {
console.log(args);
}
joinNumAndString("a", 1);
参数也可以使用readonly进行修饰.
此外函数返回有void和never类型.前者表示没有返回值(或undefined)后者表示不会退出,常用于丢错误或循环.
函数重载
不同于其他语言重载,
有一些编程语言允许不同的函数参数,对应不同的函数实现。但是,JavaScript 函数只能有一个实现,必须在这个实现当中,处理不同的参数。因此,函数体内部就需要判断参数的类型及个数,并根据判断结果执行不同的操作。
1 | function reverse(str: string): string; |
重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明
构造函数1
2
3
4
5
6type AnimalConstructor = new () => Animal;
function create(c: AnimalConstructor): Animal {
return new c();
}
create(Animal);
构造函数的类型写法,就是在参数列表前面加上new
命令
此外也有对象形式写法1
2
3type F = {
new (s: string): object;
}
针对对象,既可以使用type
别名也可以使用interface
1
2
3
4
5
6
7
8
9interface ReadOnlyPerson {
readonly name: string;
readonly age: number;
}
let w:ReadOnlyPerson = {
name:"John",
age: 22
}
空对象是 TypeScript 的一种特殊值,也是一种特殊类型。
TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。
1 | const obj = {}; |
因为Object
可以接受各种类型的值,而空对象是Object
类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。1
2
3
4
5
6
7
8
9
10
11interface Empty {}
const b: Empty = { myProp: 1, anotherProp: 2 }; // 正确
b.myProp; // 报错
let d: {};
// 等同于
// let d:Object;
d = {};
d = { x: 1 };
d = "hello";
d = 2;
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。1
2
3
4
5interface Person {
firstName: string;
lastName: string;
age: number;
}
interface 可以表示对象的各种语法,它的成员有 5 种形式。
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
interface 与 type 的区别有下面几点。
(1)type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。
(2)interface
可以继承其他类型,type
不支持继承。
可以在interface中写方法以及利用interface写函数,构造函数.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
interface Add {
(x: number, y: number): number;
}
const myAdd: Add = (x, y) => x + y;
interface ErrorConstructor {
new (message?: string): Error;
}
interface可以实现继承,而type不行.而且可以多继承.多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错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
28interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
}
type Country = {
name: string;
capital: string;
};
interface CountryWithPop extends Country {
population: number;
}
注意,如果type
命令定义的类型不是对象,interface 就无法继承
多个同名接口会进行合并.1
2
3
4
5
6
7
8interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
举例来说,Web 网页开发经常会对
windows
对象和document
对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。
1 | interface A { |
如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型1
2
3
4
5
6
7
8
9
10
11interface Circle {
area: bigint;
}
interface Rectangle {
area: number;
}
declare const s: Circle | Rectangle;
s.area; // bigint | number
类
对于顶层声明的属性,可以在声明时同时给出类型,如果不给声明默认any.1
2
3
4class Point {
x: number;
y: number;
}
TypeScript 有一个配置项
strictPropertyInitialization
,只要打开,就会检查属性是否设置了初值,如果没有就报错。
如果打开了这个设置,但是某些情况下,不是在声明时赋值或在构造方法里面赋值,为了防止这个设置报错,可以使用非空断言。1
2
3
4class Point {
x!: number;
y!: number;
}
泛型类1
2
3
4
5
6
7
8
9
10
11
12
13class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b: Box<string> = new Box("hello!");
class Pair<K, V> {
key: K;
value: V;
}
抽象类1
2
3
4
5
6
7abstract class A {
foo: number;
}
abstract class B extends A {
bar: string;
}
抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract
关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
泛型
1 | function getFirst<Type>(arr: Type[]): Type { |
不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断,有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出.
类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用
T
(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。
泛型主要用在四个场合:函数、接口、类和别名。1
2
3
4
5
6
7
8
9
10
11
12
13function id<T>(arg: T): T {
return arg;
}
function id<T>(arg: T): T {
return arg;
}
let myid: <T>(arg: T) => T = id;
interface Box<Type> {
contents: Type;
}
let box: Box<string>;
类型别名1
2
3
4
5
6
7
8type Nullable<T> = T | undefined | null
type Container<T> = { value: T };
type Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
};
类型参数默认值1
2
3function getFirst_<T = string>(arr: T[]): T {
return arr[0];
}
类型参数的约束条件1
2
3
4
5
6
7
8
9function comp<Type extends { length: number }>(a: Type, b: Type) {
if (a.length > b.length) {
return a;
}
return b;
}
type Fn<A extends string, B extends string = "world"> = [A, B];
type Result = Fn<"hello">
类型参数的约束条件如下1
<TypeParameter extends ConstraintType>
泛型使用注意:
- 尽量少用泛型
- 类型参数越少越好
- 类型参数需要出现两次
- 泛型可以嵌套
Enum类型
1 | enum Color { |
Enum 结构本身也是一种类型。比如,上例的变量c
等于1
,它的类型可以是 Color,也可以是number
多个同名的 Enum 结构会自动合并。1
2
3
4
5
6
7
8
9
10
11
12
13
14const enum MediaTypes {
JSON = "application/json",
XML = "application/xml",
}
const url = "localhost";
fetch(url, {
headers: {
Accept: MediaTypes.JSON,
},
}).then((response) => {
// ...
});
类型断言
1 | // 语法一 |
类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型
此外还有as const断言,s const
断言只能用于字面量,as const
也不能用于表达式
或者先断言为unknown.1
expr as unknown as T;
对于那些可能为空的变量(即可能等于undefined
或null
),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!
1
const root = document.getElementById("root")!;
断言函数1
2
3function isString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}
模块和namespace
TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。1
export type Bool = true | false;
模块加载方式有classic和Node,也就是Command js和ES6.
namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用。
它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace 了。
1 | namespace Utils { |
如果要在命名空间以外使用内部成员,就必须为该成员加上export
前缀,表示对外输出该成员1
2
3
4
5
6
7
8
9
10
11namespace Utility {
export function log(msg: string) {
console.log(msg);
}
export function error(msg: string) {
console.error(msg);
}
}
Utility.log("Call me");
Utility.error("maybe!");
装饰器
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@
,后面是一个表达式。
(2)@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
装饰器函数和装饰器方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24type Decorator = (
value: DecoratedValue,
context: {
kind: string;
name: string | symbol;
addInitializer?(initializer: () => void): void;
static?: boolean;
private?: boolean;
access: {
get?(): unknown;
set?(value: unknown): void;
};
}
) => void | ReplacementValue;
type ClassDecorator = (
value: Function,
context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () => void): void;
}
) => Function | void;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function countInstances(value: any, context: any) {
let instanceCount = 0;
const wrapper = function (...args: any[]) {
instanceCount++;
const instance = new value(...args);
instance.count = instanceCount;
return instance;
} as unknown as typeof MyClass;
wrapper.prototype = value.prototype; // A
return wrapper;
}
class MyClass {}
const inst1 = new MyClass();
inst1 instanceof MyClass; // true
inst1.count; // 1
declare关键字
declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。
它的主要作用,就是让当前文件可以使用其他文件声明的类型。举例来说,自己的脚本使用外部库定义的函数,编译器会因为不知道外部函数的类型定义而报错,这时就可以在自己的脚本里面使用declare
关键字,告诉编译器外部函数的类型。这样的话,编译单个脚本就不会因为使用了外部类型而报错。
declare 关键字可以描述以下类型。
- 变量(const、let、var 命令声明)
- type 或者 interface 命令声明的类型
- class
- enum
- 函数(function)
- 模块(module)
- 命名空间(namespace)
1 |
|
d.ts类型声明文件
可以为每个模块脚本,定义一个.d.ts
文件,把该脚本用到的类型定义都放在这个文件里面。但是,更方便的做法是为整个项目,定义一个大的.d.ts
文件,在这个文件里面使用declare module
定义每个模块脚本的类型
使用时,自己的脚本使用三斜杠命令,加载这个类型声明文件。1
/// <reference path="node.d.ts"/>
如果没有上面这一行命令,自己的脚本使用外部模块时,就需要在脚本里面使用 declare 命令单独给出外部模块的类型。
单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。
类型声明文件里面只有类型代码,没有具体的代码实现。它的文件名一般为
[模块名].d.ts
的形式,其中的d
表示 declaration(声明)
1 | /// <reference path="node.d.ts"/> |
1 | // node.d.ts |
最后推荐两个练习网站:
- TypeHero
- type-challenges/type-challenges: Collection of TypeScript type challenges with online judge (github.com)