文章时效性提示
这是一篇发布于 512 天前的文章,部分信息可能已发生改变,请注意甄别。
早该学学了.
之前写过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 | let obj: Object; |
此外Object类型包括除了undefined和null的基本类型.所以这并不符合直觉,推荐使用object
1 | let obj3:object; |
object类型包含对象,数组,函数.
1 | const ccx = { foo: 1 }; |
此外undefined和null也可以赋值为number,object等等.
TypeScript中单个值也是类型成为值类型
1 | let t: "dfasdf"; |
将多个类型组合起来就是联合类型,如果严格检查也就是设置strictNullChecks
,使得其他类型变量不能被赋值为undefined或null.这个时候就可以用联合类型
1 | let setting: true | false; |
对象的合成可以给对象添加新的属性,属于交叉类型.
1 | let obj5: { foo: string } & { bar: number }; |
类型别名
1 | type Age = number; |
跟Python的typing和Go语言类似.
数组 元组
1 | let arr: number[] = []; |
const数组中的元素是可以改变的,所以在ts中增加了readonly
,readonly数组是原本数组的子类型.
1 | const arr5: number[] = [0, 1]; |
声明readonly数组
1 | let aa: readonly number[] = [1, 2, 3]; |
TypeScript 推断类型时,遇到
const
命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
const
命令声明的变量,如果赋值为对象,并不会推断为值类型,这是因为 JavaScript 里面,const
变量赋值为对象时,属性值是可以改变的(数组等同理)
元组tuple
1 | const s: [string, string, boolean] = ["a", "b", true]; |
使用元组时必须声明类型不然会默认数组.
1 | let ot: [number, string?] | undefined = [1]; |
使用扩展运算符可以不下成员数量的元组.
元组也有只读元组
1 | let readonlyTuple: readonly [number] = [1]; |
symbol类型
symbol主要用于类的属性.
ts增加了unique symbol作为symbol的子类型.
1 | // 正确 |
感觉平常可能用不上…
函数 对象 interface
1 | function hello(txt: string): void { |
函数声明与函数变量声明.前者需要声明参数类型,否则默认为any.后者可以在选择在赋值时写出类型或者在声明变量时添加类型.此外还有这种写法
1 | let add: { |
箭头函数
1 | const repeat = (str: string, times: number): string => str.repeat(times); |
另外使用?表示可选参数
1 | function f(x?: number) { |
默认值也类似.
1 | function createPoint(x: number = 0, y: number = 0): [number, number] { |
rest参数也可以用于将多个值包裹为数组或元组
1 | function joinNum(...nums: [...number[]]): string { |
参数也可以使用readonly进行修饰.
此外函数返回有void和never类型.前者表示没有返回值(或undefined)后者表示不会退出,常用于丢错误或循环.
函数重载
不同于其他语言重载,
有一些编程语言允许不同的函数参数,对应不同的函数实现。但是,JavaScript 函数只能有一个实现,必须在这个实现当中,处理不同的参数。因此,函数体内部就需要判断参数的类型及个数,并根据判断结果执行不同的操作。
1 | function reverse(str: string): string; |
重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明
构造函数
1 | type AnimalConstructor = new () => Animal; |
构造函数的类型写法,就是在参数列表前面加上new
命令
此外也有对象形式写法
1 | type F = { |
针对对象,既可以使用type
别名也可以使用interface
1 | interface ReadOnlyPerson { |
空对象是 TypeScript 的一种特殊值,也是一种特殊类型。
TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。
1 | const obj = {}; |
因为Object
可以接受各种类型的值,而空对象是Object
类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。
1 | interface Empty {} |
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。
1 | interface Person { |
interface 可以表示对象的各种语法,它的成员有 5 种形式。
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
interface 与 type 的区别有下面几点。
(1)type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。
(2)interface
可以继承其他类型,type
不支持继承。
可以在interface中写方法以及利用interface写函数,构造函数.
1 | // 写法一 |
interface可以实现继承,而type不行.而且可以多继承.多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错
1 | interface Shape { |
注意,如果type
命令定义的类型不是对象,interface 就无法继承
多个同名接口会进行合并.
1 | interface Box { |
举例来说,Web 网页开发经常会对
windows
对象和document
对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。
1 | interface A { |
如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型
1 | interface Circle { |
类
对于顶层声明的属性,可以在声明时同时给出类型,如果不给声明默认any.
1 | class Point { |
TypeScript 有一个配置项
strictPropertyInitialization
,只要打开,就会检查属性是否设置了初值,如果没有就报错。
如果打开了这个设置,但是某些情况下,不是在声明时赋值或在构造方法里面赋值,为了防止这个设置报错,可以使用非空断言。
1 | class Point { |
泛型类
1 | class Box<Type> { |
抽象类
1 | abstract class A { |
抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract
关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
泛型
1 | function getFirst<Type>(arr: Type[]): Type { |
不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断,有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出.
类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用
T
(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。
泛型主要用在四个场合:函数、接口、类和别名。
1 | function id<T>(arg: T): T { |
类型别名
1 | type Nullable<T> = T | undefined | null |
类型参数默认值
1 | function getFirst_<T = string>(arr: T[]): T { |
类型参数的约束条件
1 | function comp<Type extends { length: number }>(a: Type, b: Type) { |
类型参数的约束条件如下
1 | <TypeParameter extends ConstraintType> |
泛型使用注意:
- 尽量少用泛型
- 类型参数越少越好
- 类型参数需要出现两次
- 泛型可以嵌套
Enum类型
1 | enum Color { |
Enum 结构本身也是一种类型。比如,上例的变量c
等于1
,它的类型可以是 Color,也可以是number
多个同名的 Enum 结构会自动合并。
1 | const enum MediaTypes { |
类型断言
1 | // 语法一 |
类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型
此外还有as const断言,s const
断言只能用于字面量,as const
也不能用于表达式
或者先断言为unknown.
1 | expr as unknown as T; |
对于那些可能为空的变量(即可能等于undefined
或null
),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!
1 | const root = document.getElementById("root")!; |
断言函数
1 | function isString(value: unknown): asserts value is 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 | namespace Utility { |
装饰器
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@
,后面是一个表达式。
(2)@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
装饰器函数和装饰器方法
1 | type Decorator = ( |
1 | function countInstances(value: any, context: any) { |
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)