TypeScript 学习
记录一些学习的 TS 语法。学习内容来自 阮一峰
值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。
let x:'hello';
x = 'hello'; // 正确
x = 'world'; // 报错上面示例中,变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。
TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
// x 的类型是 "https"
const x = 'https';
// y 的类型是 string
const y:string = 'https';上面示例中,变量x是const命令声明的,TypeScript 就会推断它的类型是值https,而不是string类型。
这样推断是合理的,因为const命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。
注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。
// x 的类型是 { foo: number }
const x = { foo: 1 };值类型可能会出现一些很奇怪的报错。
const x:5 = 4 + 1; // 报错上面示例中,等号左侧的类型是数值5,等号右侧4 + 1的类型,TypeScript 推测为number。由于5是number的子类型,number是5的父类型,父类型不能赋值给子类型,所以报错了
但是,反过来是可以的,子类型可以赋值给父类型。
let x:5 = 5;
let y:number = 4 + 1;
x = y; // 报错
y = x; // 正确如果一定要让子类型可以赋值为父类型的值,就要用到类型断言
const x:5 = (4 + 1) as 5; // 正确交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。
交叉类型A&B表示,任何一个类型必须同时属于A和B,才属于交叉类型A&B,即交叉类型同时满足A和B的特征。
let x:number&string;上面示例中,变量x同时是数值和字符串,这当然是不可能的,所以 TypeScript 会认为x的类型实际是never。
交叉类型的主要用途是表示对象的合成。
let obj:
{ foo: string } &
{ bar: string };
obj = {
foo: 'hello',
bar: 'world'
};上面示例中,变量obj同时具有属性foo和属性bar。
交叉类型常常用来为对象类型添加新属性。
type A = { foo: number };
type B = A & { bar: number };上面示例中,类型B是一个交叉类型,用来在A的基础上增加了属性bar。
type 命令
type命令用来定义一个类型的别名。
type Age = number;
let age:Age = 55;上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型。
别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。
别名不允许重名。
type Color = 'red';
type Color = 'blue'; // 报错上面示例中,同一个别名Color声明了两次,就报错了。
别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。
type Color = 'red';
if (Math.random() < 0.5) {
type Color = 'blue';
}上面示例中,if代码块内部的类型别名Color,跟外部的Color是不一样的。
别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。
type World = "world";
type Greeting = `hello ${World}`;上面示例中,别名Greeting使用了模板字符串,读取另一个别名World。
type命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。
interface
interface 可以使用extends关键字,继承其他 interface。
interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}interface 允许多重继承。
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
}上面示例中,Circle同时继承了Style和Shape,所以拥有三个属性color、name和radius。
多重接口继承,实际上相当于多个父接口的合并。
interface 可以继承type命令定义的对象类型。
type Country = {
name: string;
capital: string;
}
interface CountryWithPop extends Country {
population: number;
}上面示例中,CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性。
注意,如果type命令定义的类型不是对象,interface 就无法继承。
多个同名接口会合并成一个接口。
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}上面示例中,两个Box接口会合并成一个接口,同时有height、width和length三个属性。
* interface 与 type 的异同
interface命令与type命令作用类似,都可以表示对象类型。
很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
它们的相似之处,首先表现在都能为对象类型起名。
type Country = {
name: string;
capital: string;
}
interface Coutry {
name: string;
capital: string;
}上面示例是type命令和interface命令,分别定义同一个类型。
class命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用type或interface。
interface 与 type 的区别有下面几点。
(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。
(2)interface可以继承其他类型,type不支持继承。
继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}上面示例中,类型Bear在Animal的基础上添加了一个属性honey。
上例的&运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。
作为比较,interface添加属性,采用的是继承的写法。
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}继承时,type 和 interface 是可以换用的。interface 可以继承 type。
type Foo = { x: number; };
interface Bar extends Foo {
y: number;
}type 也可以继承 interface。
interface Foo {
x: number;
}
type Bar = Foo & { y: number; };(3)同名interface会自动合并,同名type则会报错。也就是说,TypeScript 不允许使用type多次定义同一个类型。
type A = { foo:number }; // 报错
type A = { bar:number }; // 报错上面示例中,type两次定义了类型A,导致两行都会报错。
作为比较,interface则会自动合并。
interface A { foo:number };
interface A { bar:number };
const obj:A = {
foo: 1,
bar: 1
};上面示例中,interface把类型A的两个定义合并在一起。
这表明,interface 是开放的,可以添加属性,type 是封闭的,不能添加属性,只能定义新的 type。
(4)interface不能包含属性映射(mapping),type可以。
interface Point {
x: number;
y: number;
}
// 正确
type PointCopy1 = {
[Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
[Key in keyof Point]: Point[Key];
};(5)this关键字只能用于interface。
// 正确
interface Foo {
add(num:number): this;
};
// 报错
type Foo = {
add(num:number): this;
};上面示例中,type 命令声明的方法add(),返回this就报错了。interface 命令没有这个问题。
下面是返回this的实际对象的例子。
class Calculator implements Foo {
result = 0;
add(num:number) {
this.result += num;
return this;
}
}(6)type 可以扩展原始数据类型,interface 不行。
// 正确
type MyStr = string & {
type: 'new'
};
// 报错
interface MyStr extends string {
type: 'new'
}上面示例中,type 可以扩展原始数据类型 string,interface 就不行。
(7)interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以。
type A = { /* ... */ };
type B = { /* ... */ };
type AorB = A | B;
type AorBwithName = AorB & {
name: string
};上面示例中,类型AorB是一个联合类型,AorBwithName则是为AorB添加一个属性。这两种运算,interface都没法表达。
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用。
class
1. 属性索引
类允许定义属性索引。
class MyClass {
[s:string]: boolean |
((s:string) => boolean);
get(s:string) {
return this[s] as boolean;
}
}上面示例中,[s:string]表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。
注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
class MyClass {
[s:string]: boolean;
f() { // 报错
return true;
}
}上面示例中,属性索引的类型里面不包括方法,导致后面的方法f()定义直接报错。正确的写法是下面这样。
class MyClass {
[s:string]: boolean | (() => boolean);
f() {
return true;
}
}属性存取器视同属性。
class MyClass {
[s:string]: boolean;
get isInstance() {
return true;
}
}上面示例中,属性inInstance的读取器虽然是一个函数方法,但是视同属性,所以属性索引虽然没有涉及方法类型,但是不会报错。
2. 类的 interface 接口
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
interface Country {
name:string;
capital:string;
}
// 或者
type Country = {
name:string;
capital:string;
}
class MyCountry implements Country {
name = '';
capital = '';
}TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
class A {
x:number = 1;
}
interface A {
y:number;
}
let a = new A();
a.y = 10;
a.x // 1
a.y // 10上面示例中,类A与接口A同名,后者会被合并进前者的类型定义。
3. 结构类型原则
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
class Person {
name: string;
}
class Customer {
name: string;
}
// 正确
const cust:Customer = new Person();上面示例中,Person和Customer是两个结构相同的类,TypeScript 将它们视为相同类型,因此Person可以用在类型为Customer的场合。
对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: Any
}
class AnimalHouse {
resident: Animal;
constructor(animal:Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
resident: Dog;
constructor(dog:Dog) {
super(dog);
}
}上面示例中,类DogHouse的顶层成员resident只设置了类型(Dog),没有设置初值。这段代码在不同的编译设置下,编译结果不一样。
如果编译设置的target设成大于等于ES2022,或者useDefineForClassFields设成true,那么下面代码的执行结果是不一样的。
const dog = {
animalStuff: 'animal',
dogStuff: 'dog'
};
const dogHouse = new DogHouse(dog);
console.log(dogHouse.resident) // undefined解决方法就是使用declare命令,去声明顶层成员的类型,告诉 TypeScript 这些成员的赋值由基类实现。
class DogHouse extends AnimalHouse {
declare resident: Dog;
constructor(dog:Dog) {
super(dog);
}
}上面示例中,resident属性的类型声明前面用了declare命令,这样就能确保在编译目标大于等于ES2022时(或者打开useDefineForClassFields时),代码行为正确。
4. 实例属性的简写形式
实际开发中,很多实例属性的值,是通过构造方法传入的。
class Point {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}上面实例中,属性x和y的值是通过构造方法的参数传入的。
这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,TypeScript 就提供了一种简写形式。
class Point {
constructor(
public x:number,
public y:number
) {}
}
const p = new Point(10, 10);
p.x // 10
p.y // 10上面示例中,构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。注意,这里的public不能省略。
除了public修饰符,构造方法的参数名只要有private、protected、readonly修饰符,都会自动声明对应修饰符的实例属性。
class A {
constructor(
public a: number,
protected b: number,
private c: number,
readonly d: number
) {}
}
// 编译结果
class A {
a;
b;
c;
d;
constructor(a, b, c, d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
}上面示例中,从编译结果可以看到,构造方法的a、b、c、d会生成对应的实例属性。
readonly还可以与其他三个可访问性修饰符,一起使用。
class A {
constructor(
public readonly x:number,
protected readonly y:number,
private readonly z:number
) {}
}5. 静态成员
类的内部可以使用static关键字,定义静态成员。
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
MyClass.x // 0
MyClass.printX() // 0上面示例中,x是静态属性,printX()是静态方法。它们都必须通过MyClass获取,而不能通过实例对象调用。
static关键字前面可以使用 public、private、protected 修饰符。
class MyClass {
private static x = 0;
}
MyClass.x // 报错上面示例中,静态属性x前面有private修饰符,表示只能在MyClass内部使用,如果在外部调用这个属性就会报错。
静态私有属性也可以用 ES6 语法的#前缀表示,上面示例可以改写如下。
class MyClass {
static #x = 0;
}注意,静态成员不能使用泛型的类型参数。
class Box<Type> {
static defaultContents: Type; // 报错
}6.抽象类,抽象成员
TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。
abstract class A {
id = 1;
}
const a = new A(); // 报错上面示例中,直接新建抽象类的实例,会报错。
抽象类只能当作基类使用,用来在它的基础上定义子类。
abstract class A {
id = 1;
}
class B extends A {
amount = 100;
}
const b = new B();
b.id // 1
b.amount // 100抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
abstract class A {
foo:number;
}
abstract class B extends A {
bar:string;
}抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
abstract class A {
abstract foo:string;
bar:string = '';
}
class B extends A {
foo = 'b';
}上面示例中,抽象类A定义了抽象属性foo,子类B必须实现这个属性,否则会报错。
下面是抽象方法的例子。如果抽象类的方法前面加上abstract,就表明子类必须给出该方法的实现。
abstract class A {
abstract execute():string;
}
class B extends A {
execute() {
return `B executed`;
}
}这里有几个注意点。
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。
(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。
(4)一个子类最多只能继承一个抽象类。
总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。
7. this 问题
类的方法经常用到this关键字,它表示该方法当前所在的对象。
class A {
name = 'A';
getName() {
return this.name;
}
}
const a = new A();
a.getName() // 'A'
const b = {
name: 'b',
getName: a.getName
};
b.getName() // 'b'上面示例中,变量a和b的getName()是同一个方法,但是执行结果不一样,原因就是它们内部的this指向不一样的对象。如果getName()在变量a上运行,this指向a;如果在b上运行,this指向b。
关于 b 的理解为下方代码
const b = {
name: 'b',
getName: a.getName
};
等同于
const b = {
name: 'b',
getName: () => {
return this.name;
}
};
b.getName() // 'b'有些场合需要给出this类型,但是 JavaScript 函数通常不带有this参数,这时 TypeScript 允许函数增加一个名为this的参数,放在参数列表的第一位,用来描述函数内部的this关键字的类型。
// 编译前
function fn(
this: SomeType,
x: number
) {
/* ... */
}
// 编译后
function fn(x) {
/* ... */
}上面示例中,函数fn()的第一个参数是this,用来声明函数内部的this的类型。编译时,TypeScript 一旦发现函数的第一个参数名为this,则会去除这个参数,即编译结果不会带有该参数。
class A {
name = 'A';
getName(this: A) {
return this.name;
}
}
const a = new A();
const b = a.getName;
b() // 报错上面示例中,类A的getName()添加了this参数,如果直接调用这个方法,this的类型就会跟声明的类型不一致,从而报错。
上述理解:
b() // 报错
此时 b 是一个函数
function getName(this: A) {
return this.name;
}
直接调用 b() 此时参数类型为 void 因为 b 没有调用类型
需要将 b 绑定一个 类型为 A 的调用者
const bb = b.bind(a)
console.log(bb())
这时 bb() 调用 this 为 a 类型是 Athis参数的类型可以声明为各种对象。
function foo(
this: { name: string }
) {
this.name = 'Jack';
this.name = 0; // 报错
}
foo.call({ name: 123 }); // 报错上面示例中,参数this的类型是一个带有name属性的对象,不符合这个条件的this都会报错。
TypeScript 提供了一个noImplicitThis编译选项。如果打开了这个设置项,如果this的值推断为any类型,就会报错。
// noImplicitThis 打开
class Rectangle {
constructor(
public width:number,
public height:number
) {}
getAreaFunction() {
return function () {
return this.width * this.height; // 报错
};
}
}上面示例中,getAreaFunction()方法返回一个函数,这个函数里面用到了this,但是这个this跟Rectangle这个类没关系,它的类型推断为any,所以就报错了。
在类的内部,this本身也可以当作类型使用,表示当前类的实例对象。
class Box {
contents:string = '';
set(value:string):this {
this.contents = value;
return this;
}
}上面示例中,set()方法的返回值类型就是this,表示当前的实例对象。
注意,this类型不允许应用于静态成员。
class A {
static a:this; // 报错
}上面示例中,静态属性a的返回值类型是this,就报错了。原因是this类型表示实例对象,但是静态成员拿不到实例对象。
有些方法返回一个布尔值,表示当前的this是否属于某种类型。这时,这些方法的返回值类型可以写成this is Type的形式,其中用到了is运算符。
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
// ...
}泛型
1.简介
有些时候,函数返回值的类型与参数类型是相关的。
function getFirst(arr) {
return arr[0];
}上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。
这个函数的类型声明只能写成下面这样。
function f(arr:any[]):any {
return arr[0];
}上面的类型声明,就反映不出参数与返回值之间的类型关系。
为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。
function getFirst<T>(arr:T[]):T {
return arr[0];
}上面示例中,函数getFirst()的函数名后面尖括号的部分<T>,就是类型参数,参数要放在一对尖括号(<>)里面。本例只有一个类型参数T,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。
上例的函数getFirst()的参数类型是T[],返回值类型是T,就清楚地表示了两者之间的关系。比如,输入的参数类型是number[],那么 T 的值就是number,因此返回值类型也是number。
函数调用时,需要提供类型参数。
getFirst<number>([1, 2, 3])上面示例中,调用函数getFirst()时,需要在函数名后面使用尖括号,给出类型参数T的值,本例是<number>。
不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。
getFirst([1, 2, 3])上面示例中,TypeScript 会从实际参数[1, 2, 3],推断出类型参数 T 的值为number。
类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。
下面是多个类型参数的例子。
function map<T, U>(
arr:T[],
f:(arg:T) => U
):U[] {
return arr.map(f);
}
// 用法实例
map<string, number>(
['1', '2', '3'],
(n) => parseInt(n)
); // 返回 [1, 2, 3]上面示例将数组的实例方法map()改写成全局函数,它有两个类型参数T和U。含义是,原始数组的类型为T[],对该数组的每个成员执行一个处理函数f,将类型T转成类型U,那么就会得到一个类型为U[]的数组。
总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。
2.泛型的写法
2.1 函数的泛型写法
function id<T>(arg:T):T {
return arg;
}那么对于变量形式定义的函数,泛型有下面两种写法。
// 写法一
let myId:<T>(arg:T) => T = id;
// 写法二
let myId:{ <T>(arg:T): T } = id;
// 理解 let instanceType: Type = instanceType
// 写法一
type AT = <T>(arg:T) => T;
let myId:AT = id;
// 写法二 解构函数 详见 文档 https://wangdoc.com/typescript/function
type AT = { <T>(arg:T): T } ;
let myId:AT = id;2.2 接口的泛型写法
interface Comparator<T> {
compareTo(value:T): number;
}
class Rectangle implements Comparator<Rectangle> {
compareTo(value:Rectangle): number {
// ...
}
}上面示例中,先定义了一个泛型接口,然后将这个接口用于一个类。
实例:可以对不同的类型 用不同的实现 更加清晰每个类型处理
2.3 类的泛型写法
class C<NumType> {
value!: NumType;
add!: (x: NumType, y: NumType) => NumType;
}
let foo = new C<number>();
foo.value = 0;
foo.add = function (x, y) {
return x + y;
};2.4 类型别名的泛型写法
type 命令定义的类型别名,也可以使用泛型。
type Nullable<T> = T | undefined | null;上面示例中,Nullable<T>是一个泛型,只要传入一个类型,就可以得到这个类型与undefined和null的一个联合类型。
下面是另一个例子。
type Container<T> = { value: T };
const a: Container<number> = { value: 0 };
const b: Container<string> = { value: 'b' };下面是定义树形结构的例子。
type Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
};上面示例中,类型别名Tree内部递归引用了Tree自身。
3. 类型参数的默认值
类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。
function getFirst<T = string>(
arr:T[]
):T {
return arr[0];
}上面示例中,T = string表示类型参数的默认值是string。调用getFirst()时,如果不给出T的值,TypeScript 就认为T等于string。
但是,因为 TypeScript 会从实际参数推断出T的值,从而覆盖掉默认值,所以下面的代码不会报错。
getFirst([1, 2, 3]) // 正确上面示例中,实际参数是[1, 2, 3],TypeScript 推断 T 等于number,从而覆盖掉默认值string。
类型参数的默认值,往往用在类中。
class Generic<T = string> {
list:T[] = []
add(t:T) {
this.list.push(t)
}
}上面示例中,类Generic有一个类型参数T,默认值为string。这意味着,属性list默认是一个字符串数组,方法add()的默认参数是一个字符串。
const g = new Generic();
g.add(4) // 报错
g.add('hello') // 正确上面示例中,新建Generic的实例g时,没有给出类型参数T的值,所以T就等于string。因此,向add()方法传入一个数值会报错,传入字符串就不会。
const g = new Generic<number>();
g.add(4) // 正确
g.add('hello') // 报错上面示例中,新建实例g时,给出了类型参数T的值是number,因此add()方法传入数值不会报错,传入字符串会报错。
一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。
<T = boolean, U> // 错误
<T, U = boolean> // 正确上面示例中,依次有两个类型参数T和U。如果T是可选参数,U不是,就会报错。
4. 类型参数的约束条件
很多类型参数并不是无限制的,对于传入的类型存在约束条件。
function comp<Type>(a:Type, b:Type) {
if (a.length >= b.length) {
return a;
}
return b;
}上面示例中,类型参数 Type 有一个隐藏的约束条件:它必须存在length属性。如果不满足这个条件,就会报错。
TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。
function comp<T extends { length: number }>(
a: T,
b: T
) {
if (a.length >= b.length) {
return a;
}
return b;
}上面示例中,T extends { length: number }就是约束条件,表示类型参数 T 必须满足{ length: number },否则就会报错。
comp([1, 2], [1, 2, 3]) // 正确
comp('ab', 'abc') // 正确
comp(1, 2) // 报错上面示例中,只要传入的参数类型不满足约束条件,就会报错。
类型参数的约束条件采用下面的形式。
<TypeParameter extends ConstraintType>上面语法中,TypeParameter表示类型参数,extends是关键字,这是必须的,ConstraintType表示类型参数要满足的条件,即类型参数应该是ConstraintType的子类型。
类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。
type Fn<A extends string, B extends string = 'world'>
= [A, B];
type Result = Fn<'hello'> // ["hello", "world"]5.泛型可以嵌套。
类型参数可以是另一个泛型。
type OrNull<Type> = Type|null;
type OneOrMany<Type> = Type|Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;上面示例中,最后一行的泛型OrNull的类型参数,就是另一个泛型OneOrMany。
类型断言
1. 类型断言有两种语法。
// 语法一:<类型>值
<Type>value
// 语法二:值 as 类型
value as Type上面两种语法是等价的,value表示值,Type表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。
// 语法一
let bar:T = <T>foo;
// 语法二
let bar:T = foo as T;上面示例是两种类型断言的语法,其中的语法一因为跟 JSX 语法冲突,使用时必须关闭 TypeScript 的 React 支持,否则会无法识别。由于这个原因,现在一般都使用语法二。
2.类型断言的条件
const n = 1;
const m:string = n as string; // 报错类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
但是,如果真的要断言成一个完全无关的类型,也是可以做到的。
// 或者写成 <T><unknown>expr
expr as unknown as T上面代码中,expr连续进行了两次类型断言,第一次断言为unknown类型,第二次断言为T类型。这样的话,expr就可以断言成任意类型T,而不报错。
下面是本小节开头那个例子的改写。
const n = 1;
const m:string = n as unknown as string; // 正确3. as const 断言
如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。
// 类型推断为基本类型 string
let s1 = 'JavaScript';
// 类型推断为字符串 “JavaScript”
const s2 = 'JavaScript';上面示例中,变量s1的类型被推断为string,变量s2的类型推断为值类型JavaScript。后者是前者的子类型,相当于 const 命令有更强的限定作用,可以缩小变量的类型范围。
有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。
let s = 'JavaScript';
type Lang =
|'JavaScript'
|'TypeScript'
|'Python';
function setLang(language:Lang) {
/* ... */
}
setLang(s); // 报错上面示例中,最后一行报错,原因是函数setLang()的参数language类型是Lang,这是一个联合类型。但是,传入的字符串s的类型被推断为string,属于Lang的父类型。父类型不能替代子类型,导致报错。
一种解决方法就是把 let 命令改成 const 命令。
const s = 'JavaScript';这样的话,变量s的类型就是值类型JavaScript,它是联合类型Lang的子类型,传入函数setLang()就不会报错。
另一种解决方法是使用类型断言。TypeScript 提供了一种特殊的类型断言as const,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。
let s = 'JavaScript' as const;
setLang(s); // 正确上面示例中,变量s虽然是用 let 命令声明的,但是使用了as const断言以后,就等同于是用 const 命令声明的,变量s的类型会被推断为值类型JavaScript。
使用了as const断言以后,let 变量就不能再改变值了。
let s = 'JavaScript' as const;
s = 'Python'; // 报错上面示例中,let 命令声明的变量s,使用as const断言以后,就不能改变值了,否则报错。
注意,as const断言只能用于字面量,不能用于变量。
let s = 'JavaScript';
setLang(s as const); // 报错上面示例中,as const断言用于变量s,就报错了。下面的写法可以更清晰地看出这一点。
let s1 = 'JavaScript';
let s2 = s1 as const; // 报错另外,as const也不能用于表达式。
let s = ('Java' + 'Script') as const; // 报错上面示例中,as const用于表达式,导致报错。
as const也可以写成前置的形式。
// 后置形式
expr as const
// 前置形式
<const>expras const断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。
const v1 = {
x: 1,
y: 2,
}; // 类型是 { x: number; y: number; }
const v2 = {
x: 1 as const,
y: 2,
}; // 类型是 { x: 1; y: number; }
const v3 = {
x: 1,
y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }上面示例中,第二种写法是对属性x缩小类型,第三种写法是对整个对象缩小类型。
总之,as const会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。
下面是数组的例子。
// a1 的类型推断为 number[]
const a1 = [1, 2, 3];
// a2 的类型推断为 readonly [1, 2, 3]
const a2 = [1, 2, 3] as const;上面示例中,数组字面量使用as const断言后,类型推断就变成了只读元组。
由于as const会将数组变成只读元组,所以很适合用于函数的 rest 参数。
function add(x:number, y:number) {
return x + y;
}
const nums = [1, 2];
const total = add(...nums); // 报错上面示例中,变量nums的类型推断为number[],导致使用扩展运算符...传入函数add()会报错,因为add()只能接受两个参数,而...nums并不能保证参数的个数。
事实上,对于固定参数个数的函数,如果传入的参数包含扩展运算符,那么扩展运算符只能用于元组。只有当函数定义使用了 rest 参数,扩展运算符才能用于数组。
解决方法就是使用as const断言,将数组变成元组。
const nums = [1, 2] as const;
const total = add(...nums); // 正确上面示例中,使用as const断言后,变量nums的类型会被推断为readonly [1, 2],使用扩展运算符展开后,正好符合函数add()的参数类型。
Enum 成员也可以使用as const断言。
enum Foo {
X,
Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X上面示例中,如果不使用as const断言,变量e1的类型被推断为整个 Enum 类型;使用了as const断言以后,变量e2的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。
4.非空断言
空断言只有在打开编译选项strictNullChecks时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为undefined或null。
?可选 !必须。 如果!强制解包错误会报错。
模块
1. 简介
任何包含 import 或 export 语句的文件,就是一个模块(module)。相应地,如果文件不包含 export 语句,就是一个全局的脚本文件。
模块本身就是一个作用域,不属于全局作用域。模块内部的变量、函数、类只在内部可见,对于模块外部是不可见的。暴露给外部的接口,必须用 export 命令声明;如果其他文件要使用模块的接口,必须用 import 命令来输入。
如果一个文件不包含 export 语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句。
export {};上面这行语句不产生任何实际作用,但会让当前文件被当作模块处理,所有它的代码都变成了内部代码。
2. import type 语句
import 在一条语句中,可以同时输入类型和正常接口。
// a.ts
export interface A {
foo: string;
}
export let a = 123;
// b.ts
import { A, a } from './a';上面示例中,文件a.ts的 export 语句输出了一个类型A和一个正常接口a,另一个文件b.ts则在同一条语句中输入了类型和正常接口。
这样很不利于区分类型和正常接口,容易造成混淆。为了解决这个问题,TypeScript 引入了两个解决方法。
第一个方法是在 import 语句输入的类型前面加上type关键字。
import { type A, a } from './a';上面示例中,import 语句输入的类型A前面有type关键字,表示这是一个类型。
第二个方法是使用 import type 语句,这个语句只能输入类型,不能输入正常接口。
// 正确
import type { A } from './a';
// 报错
import type { a } from './a';import type 语句也可以输入默认类型。
import type DefaultType from 'moduleA';import type 在一个名称空间下,输入所有类型的写法如下。
import type * as TypeNS from 'moduleA';同样的,export 语句也有两种方法,表示输出的是类型。
type A = 'a';
type B = 'b';
// 方法一
export {type A, type B};
// 方法二
export type {A, B};上面示例中,方法一是使用type关键字作为前缀,表示输出的是类型;方法二是使用 export type 语句,表示整行输出的都是类型。
下面是 export type 将一个类作为类型输出的例子。
class Point {
x: number;
y: number;
}
export type { Point };上面示例中,由于使用了 export type 语句,输出的并不是 Point 这个类,而是 Point 代表的实例类型。输入时,只能作为类型输入。
import type { Point } from './module';
const p:Point = { x: 0, y: 0 };上面示例中,Point只能作为类型输入,不能当作正常接口使用。
3. 模块定位
模块定位(module resolution)指的是一种算法,用来确定 import 语句和 export 语句里面的模块文件位置。
// 相对模块
import { TypeA } from './a';
// 非相对模块
import * as $ from "jquery";上面示例中,TypeScript 怎么确定./a或jquery到底是指哪一个模块,具体位置在哪里,用到的算法就叫做“模块定位”。
编译参数moduleResolution,用来指定具体使用哪一种定位算法。常用的算法有两种:Classic和Node。
如果没有指定moduleResolution,它的默认值与编译参数module有关。module设为commonjs时(项目脚本采用 CommonJS 模块格式),moduleResolution的默认值为Node,即采用 Node.js 的模块定位算法。其他情况下(module设为 es2015、 esnext、amd, system, umd 等等),就采用Classic定位算法。