TypeScript类型基础
类型注解
在TypeScript中,我们可以使用类型注解来明确标识类型。类型注解的语法由一个冒号“:”和某种具体类型“Type”组成,示例如下:
TypeScript中的类型注解总是放在被修饰的实体之后。示例如下:
const greeting:string = ‘Hello, World!‘;
TypeScript中的类型注解是可选的,编译器在大部分情况下都能够自动推断出表达式的类型。
const greeting = 'Hello, World!';
此例中,虽然没有给常量greeting添加类型注解,但是TypeScript仍然能够从greeting的初始值中推断出它是string类型的常量。
类型检查
类型检查是验证程序中类型约束是否正确的过程。类型检查既可以在程序编译时进行,即静态类型检查;也可以在程序运行时进行,即动态类型检查。TypeScript支持静态类型检查, JavaScript支持动态类型检查。
为了满足不同用户的需求,TypeScript提供了两种静态类型检查模式:
■非严格类型检查(默认方式)。
■严格类型检查。
非严格类型检查
非严格类型检查是TypeScript默认的类型检查模式。在该模式下,类型检查的规则相对宽松。
例如,在非严格类型检查模式下不会对undefined值和null值做过多限制,允许将undefined值和null 值赋值给string类型的变量。当进行JavaScript代码到TypeScript代码的迁移工作时,非严格类型检查是一个不错的选择,因为它能够让我们快速地完成迁移工作。
严格类型检查
该模式下的类型检查比较激进,会尽可能地发现代码中的错误。例如,在严格类型检查模式下不允许将undefined值和null值赋值给string类型的变量。启用严格类型检查模式能够最大限度地利用TypeScript静态类型检查带来的益处。从长远来讲,使用严格类型检查模式对提高代码质量更加有利,因此建议在新的工程中启用严格类型检查。
TypeScript提供了若干个与严格类型检查相关的编译选项,例如“--strictNulIChecks”和“--nolmplicitAny”等。
在学习TypeScript语言的过程中,推荐启用所有严格类型检查编译选项。如果使用TypeScript 官网提供的在线代码编辑器,那么这些严格类型检查编译选项是默认开启的。如果使用本地开发环境,那么可以在工程的tsconfig。json配置文件中启用“--strict”编译选项。示例如下:
{
"compilerOptions": {
"strict": true,
}
}
此例中,将“--strict”编译选项设置为true将开启所有的严格类型检查编译选项。它包含了前面提到的“--strictNullChecks”和“--nolmplicitAny”编译选项。
原始类型
JavaScript语言中的每种原始类型都有与之对应的TypeScript类型。除此之外,TypeScript还对原始类型进行了细化与扩展,增加了枚举类型和字面量类型等。
- boolean
- string
- number
- bigint
- symbal
- undefined
- null
- void
- 枚举类型
- 字面量类型
boolean
TypeScript中的boolean类型对应于JavaScript中的Boolean原始类型。该类型能够表示两个逻辑值:true和false。
boolean类型使用boolean关键字来表示。示例如下:
const yes:boolean = true;
const no:boolean = false;
string
TypeScript中的string类型对应于JavaScript中的String原始类型。该类型能够表示采用Unicode UTF-16编码格式存储的字符序列。
string类型使用string关键字表示。我们通常使用字符串字面量或模板字面量来创建string类型的值
const foo:string = 'foo';
const bar:string = `bar,${foo}`;
number
TypeScript中的number类型对应于JavaScript中的Number原始类型。该类型能够表示采用双精度64位二进制浮点数格式存储的数字。
number类型使用number关键字来表示。示例如下:
const integer:number = 10;
const float:number = 3.14;
bigint
TypeScript中的bigint类型对应于JavaScript中的BigInt原始类型。该类型能够表示任意精度的整数,但也仅能表示整数。bigint采用了特殊的对象数据结构来表示和存储一个整数。
bigint类型使用bigint关键字来表示。示例如下:
const integer:bigint = 10n
symbal与unique symbal
Symbol(符号)是一种原始数据类型,用于表示独一无二的标识符。可以通过Symbol()
函数来创建一个Symbol。
下面是一个示例,展示如何使用Symbol:
// 创建Symbol
const symbol1 = Symbol();
const symbol2 = Symbol("description");
// 使用Symbol作为对象属性
const obj = {
[symbol1]: "value",
};
console.log(obj[symbol1]); // 输出 "value"
// Symbol不能通过点表示法访问
// console.log(obj.symbol1); // 错误,symbol1未定义
// 在不同上下文中创建的Symbol总是不相等
console.log(symbol1 === symbol2); // 输出 false
Unique Symbol(唯一符号)是一种特殊的Symbol,用于表示全局唯一的标识符。它是通过Symbol.for(key)
函数创建的,其中key
参数是一个字符串,用于作为唯一标识符的键。
下面是一个示例,展示如何使用Unique Symbol:
// 创建唯一Symbol
const uniqueSymbol1 = Symbol.for("key");
const uniqueSymbol2 = Symbol.for("key");
console.log(uniqueSymbol1 === uniqueSymbol2); // 输出 true,因为它们使用相同的键创建
// 通过Symbol.keyFor()获取与Unique Symbol关联的键
const key = Symbol.keyFor(uniqueSymbol1);
console.log(key); // 输出 "key"
值得注意的是,Unique Symbol是全局共享的,可以在不同的上下文中通过相同的键来获取相同的Unique Symbol。而普通的Symbol每次创建都是唯一的,即使使用相同的描述字符串。
Nullable
TypeScript中的Nullable类型指的是值可以为undefined或null的类型。
TypeScript 2.0版本的一个改变就是增加了undefined类型和null类型供开发者使用。现在,在TypeScript程序中能够明确地指定某个值的类型是否为undefined类型或nulI类型。
TypeScript编译器也能够对代码进行更加细致的检查以找出程序中潜在的错误。
undefined
undefined类型使用undefined关键字标识
const a:undefined = undefined;
null
nullnul类型使用nul关键字标识
const a:null = null;
--strictNullChecks
{
"compilerOptions": {
"strict": true,
"target": "ES2015",
"lib": ["ES2015","DOM"],
"strictNullChecks": false
}
}
TypeScript 2.0还增加了一个新的编译选项“--strictNullChecks”,严格null、undefined类型检查模式,虽然编译选项名只提及null
在没有启用“--strictNullChecks”编译选项时,允许将undefined值和null值赋值给string类型等其他类型。示例如下:
// --strictNullChecks=false
let m1: boolean = undefined;
let m2: string = undefined;
let m3: number = undefined;
let m4: bigint = undefined;
let m5: symbol = undefined;
let m6: undefined = undefined;
let m7: null = undefined;
//--strictNullChecks=false
let foo:string = undefined; 正确,可以通过类型检查
foo.length; 在运行时,将产生类型错误
//运行结果:
//Error: TypeError: Cannot read property ‘length‘ of undefined
当启用了“--strictNulIChecks”编译选项时,undefined值和null值不再能够赋值给不相关的类型
//--strictNullChecks=true
let foo:string = undefined;
// 编译错误!类型‘undefined‘不能赋值给类型 ‘string‘
foo.length;
void
void类型表示某个值不存在,该类型用作函数的返回值类型。若一个函数没有返回值, 那么该函数的返回值类型为void类型。除了将void类型作为函数返回值类型外,在其他地方使用void类型是无意义的。
function log(message:string):void {
console.log(message);
}
当启用了“--strictNullChecks”编译选项时,只允许将undefined值赋值给void类型。如果没有启用“--strictNulIChecks”编译选项,那么允许将undefined值和null值赋值给void类型。
// --strictNullChecks=true
// 正确
function foo2():void{
return undefined
}
//编译错误!类型‘null‘不能赋值给类型‘void‘
function bar(): void {
return null;
}
// ==========================================
// --strictNullChecks=false
// 正确
function foo2():void{
return undefined
}
//正确
function bar(): void {
return null;
}
枚举类型
枚举类型由零个或多个枚举成员构成,每个枚举成员都是一个命名的常量。
在TypeScript中,枚举类型是一种原始类型,它通过enum关键字来定义。例如,我们可以使用枚举类型来表示一年四季,示例如下:
enum Season{
Spring,
Summer,
Fall,
Winter,
}
按照枚举成员的类型可以将枚举类型划分为以下三类:
- 数值型枚举
- 字符串枚举
- 异构型枚举
数值型枚举
数值型枚举是最常用的枚举类型,是number类型的子类型,它由一组命名的数值常量构成。定义数值型枚举的方法如下所示:
enum DirectionEnum{
Up, //0
Down, //1
Left, //2
Right, //3
}
const dirction:DirectionEnum = DirectionEnum.Down;
每个数值型枚举成员都表示一个具体的数字。如果在定义枚举时没有设置枚举成员的值,那么 TypeScript将自动计算枚举成员的值。
根据TypeScript语言的规则,第一个枚举成员的值为0,其后每个枚举成员的值等于前一个枚举成员的值加1。
在定义数值型枚举时,可以为一个或多个枚举成员设置初始值。对于未指定初始值的枚举成员,其值为前一个枚举成员的值加1。
enum DirectionEnum {
Up = 1, //1
Down, //2
Left = 10, //10
Right, //11
}
数值型枚举是number类型的子类型,因此允许将数值型枚举类型赋值给number类型。
const d3:number = DirectionEnum.Up;
字符串枚举
字符串枚举与数值型枚举相似。在字符串枚举中,枚举成员的值为字符串。字符串枚举成员必须使用字符串字面量或另一个字符串枚举成员来初始化。字符串枚举成员没有自增长的行为。示例如下:
enum DirectionEnum {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
U=Up,
D=Down,
L=Left,
R=Right,
}
字符串枚举是string类型的子类型,因此允许将字符串枚举类型赋值给string类型。
异构型枚举
TypeScript允许在一个枚举中同时定义数值型枚举成员和字符串枚举成员,我们将这种类型的枚举称作异构型枚举。异构型枚举在实际代码中很少被使用,虽然在语法上允许定义异构型枚举, 但是不推荐在代码中使用异构型枚举。我们可以尝试使用对象来代替异构型枚举。
enum DirectionEnum {
Up = "Up",
Down = "Down",
Left = "Left",
Right = "Right",
U=0,
D=1,
L=2,
R=3,
}
枚举成员映射
不论是哪种类型的枚举,都可以通过枚举成员名去访问枚举成员值。下例中,通过枚举名Bool 和枚举成员名False与True能够访问枚举成员的值:
enum Bool {
false = 0,
true = 1,
}
console.log(Bool.false);
console.log(Bool[Bool.false]);
对于数值型枚举,不但可以通过枚举成员名来获取枚举成员值,也可以反过来通过枚举成员值去获取枚举成员名。下例中,通过枚举成员值“Bool。False”能够获取其对应的枚举成员名,即字符串 “‘False“
对于字符串枚举和异构型枚举,则不能够通过枚举成员值去获取枚举成员名。
常量枚举成员和计算枚举成员
在TypeScript中,枚举成员可以是常量枚举成员或计算枚举成员。
常量枚举成员是在编译时求值的,它们的值在编译期间就确定了,并且在运行时是不可改变的。常量枚举成员只包含常量表达式(字面量、其他常量枚举成员、带括号的常量表达式和一元运算符应用),而不包含计算逻辑。
以下是一个示例,展示了常量枚举成员的使用:
enum Direction {
Up,
Down,
Left,
Right,
}
console.log(Direction.Up); // 输出 0
console.log(Direction.Down); // 输出 1
在上述示例中,Direction枚举的成员Up、Down、Left和Right都是常量枚举成员。它们的值是从0开始自动递增的。
计算枚举成员是在运行时求值的,它们的值在运行时才确定。计算枚举成员可以包含计算逻辑,可以使用运算符、函数调用和表达式等。
以下是一个示例,展示了计算枚举成员的使用:
enum Size {
Small = 1,
Medium = calculateSize("Medium"),
Large = calculateSize("Large"),
}
function calculateSize(size: string): number {
if (size === "Medium") {
return 2;
} else if (size === "Large") {
return 3;
} else {
return 0;
}
}
console.log(Size.Small); // 输出 1
console.log(Size.Medium); // 输出 2
console.log(Size.Large); // 输出 3
在上述示例中,我们定义了一个Size枚举,并使用计算枚举成员为Medium和Large成员赋予动态计算的值。计算逻辑在calculateSize函数中定义,根据传入的尺寸名称返回相应的尺寸值。
在创建Size枚举实例时,calculateSize函数会在运行时被调用,返回相应的值。
需要注意的是,常量枚举成员在编译后会被删除,只保留枚举成员的值。而计算枚举成员会被保留在编译后的JavaScript代码中。
联合枚举类型
当枚举类型中的所有成员都是字面量枚举成员时,该枚举类型成了联合枚举类型。
使用联合枚举类型来定义一个类型,它可以接受多个枚举成员中的值作为有效的取值。
enum Fruit {
Apple,
Banana,
}
enum Color {
Red,
Yellow,
}
type FruitOrColor = Fruit | Color;
function display(value: FruitOrColor) {
if (typeof value === "number") {
if (value === Fruit.Apple) {
console.log("Fruit: Apple");
} else {
console.log("Fruit: Banana");
}
} else {
if (value === Color.Red) {
console.log("Color: Red");
} else {
console.log("Color: Yellow");
}
}
}
display(Fruit.Apple); // 输出 "Fruit: Apple"
display(Fruit.Banana); // 输出 "Fruit: Banana"
display(Color.Red); // 输出 "Color: Red"
display(Color.Yellow); // 输出 "Color: Yellow"
const枚举类型
enum Direction {
Up,
Down,
Left,
Right,
}
// const a: DirectionEnum = DirectionEnum.Up;
const b = DirectionEnum.Up;
//此例中的代码编译后生成的JavaScript代码如下所示,为了支持枚举成员名与枚举成员值之间的正、反向映射关系,TypeScript还生成了一些额外的代码:
var DirectionEnum;
(function (DirectionEnum) {
DirectionEnum[DirectionEnum["Up"] = 0] = "Up";
DirectionEnum[DirectionEnum["Down"] = 1] = "Down";
DirectionEnum[DirectionEnum["Left"] = 2] = "Left";
DirectionEnum[DirectionEnum["Right"] = 3] = "Right";
})(DirectionEnum || (DirectionEnum = {}));
// const a: DirectionEnum = DirectionEnum.Up;
var a = DirectionEnum.Up;
有时候我们不会使用枚举成员值到枚举成员名的反向映射,因此没有必要生成额外的反向映射代码
const枚举类型将在编译阶段被完全删除,并且在使用了const 枚举类型的地方会直接将const枚举成员的值内联到代码中。
const enum DirectionEnum {
Up,
Down,
Left,
Right,
}
const b = DirectionEnum.Up;
//此例中的代码编译后生成的JavaScript代码如下所示
var b = 0 /* DirectionEnum.Up */;
通过使用常量枚举类型,可以在编译时优化代码,并提供更高的性能和更小的生成代码体积。但请注意,常量枚举的使用应仅限于适合的场景,因为它们不会在运行时提供真正的枚举对象。
字面量类型
boolean字面量类型
boolean字面量类型只有以下两种:
true字面量类型。
false字面量类型。
true字面量类型只能接受true值;同理, false字面量类型只能接受false值
const a:true = true;
const b:false = false;
boolean字面量类型是boolean类型的子类型,因此可以将boolean字面量类型赋值给boolean类
原始类型boolean等同于由true字面量类型和false字面量类型构成的联合类型,即:
type BooleanAlias = true | false;
string字面量类型
字符串字面量和模板字面量都能够创建字符串。字符串字面量和不带参数的模板字面量可以作为string字面量类型使用。
string字面量类型是string类型的子类型,因此可以将string字面量类型赋值给string类型。
const a:'hello'= 'hello';
const b:`world`=`world`;
数字字面量类型
数字字面量类型包含以下两类:
number字面量类型。
bigint字面量类型。
所有的二进制、八进制、十进制和十六进制数字字面量都可以作为数字字面量类型。
number字面量类型和bigint字面量类型分别是number类型和bigint类型的子类型。
const c0: 1 = 1;
const c1: 1n = 1n;
枚举成员字面量类型
const enum DirectionEnum {
Up,
Down,
Left,
Right,
}
const b:DirectionEnum.Right = DirectionEnum.Right;
单元类型
单元类型 (Unit Type) 也叫作单例类型(Singleton Type) ,指的是仅包含一个可能值的类型。由于这个特殊的性质,编译器在处理单元类型时甚至不需要关注单元类型表示的具体值。
TypeScript中的单元类型有以下几种:
undefined类型。
null类型。
unique symbol类型。
void类型。
字面量类型。
联合枚举成员类型。
我们能够看到这些单元类型均只包含一个可能值。示例如下:
const a: undefined = undefined;
const b: null = null;
const c: unique symbol = Symbol();
const d: void = undefined;
const e: "hello" = "hello";
enum FooEnum {
A,
B,
}
const f: FooEnum.A = FooEnum.A;
顶端类型
顶端类型(TopType)源自于数学中的类型论,同时它也被广泛应用于计算机编程语言中。顶端类型是一种通用类型,有时也称为通用超类型,因为在类型系统中,所有类型都是顶端类型的子类型,或者说顶端类型是所有其他类型的父类型。顶端类型涵盖了类型系统中所有可能的值。
TypeScript中有以下两种顶端类型:
any
unknown
any
any类型是从TypeScript 1.0开始就支持的一种顶端类型。any类型使用any关键字作为标识。
在TypeScript中,所有类型都是any类型的子类型。我们可以将任何类型的值赋值给any类型。
let x1: any;
x1 = true;
x1 = "hi";
x1 = 3.14;
x1 = 99999n;
x1 = Symbol();
x1 = undefined;
x1 = null;
x1 = {};
x1 = [];
x1 = function () {};
需要注意的是,虽然any类型是所有类型的父类型,但是TypeScript允许将any类型赋值给任何其他类型。
从长远来看,我们应该尽量减少在代码中使用any类型。 因为只有开发者精确地描述了类型信息,TypeScript编译器才能够更加准确有效地进行类型检查,这也是我们选择使用TypeScript语言的主要原因之一。
--nolmplicitAny
在大多数情况下,我们想要避免上述情况的发生。因此,TypeScript提供了一个“-- nolImplicitAny”编译选项来控制该行为。当启用了该编译选项时,如果发生了隐式的any类型转换, 那么会产生编译错误。
unknown
TypeScript3.0版本引入了另一种顶端类型unknown。unknown类型使用unknown关键字作为标识。
根据顶端类型的性质,任何其他类型都能够赋值给unknown类型,该行为与any类型是一致的。示例如下:
let x1: unknown;
x1 = true;
x1 = "hi";
x1 = 3.14;
x1 = 99999n;
x1 = Symbol();
x1 = undefined;
x1 = null;
x1 = {};
x1 = [];
x1 = function () {};
unknown类型是比any类型更安全的顶端类型,因为unknown类型只允许赋值给any类型和 unknown类型,而不允许赋值给任何其他类型,该行为与any类型是不同的。
unknown类型 只能赋值给any类型和unknown类型,不允许赋值给其他类型
尾端类型
在类型系统中,尾端类型(BottomType)是所有其他类型的子类型。由于一个值不可能同时属于所有类型,例如一个值不可能同时为数字类型和字符串类型,因此尾端类型中不包含任何值。
尾端类型也称作0类型或者空类型。
TypeScript中只存在一种尾端类型,即never类型。
never
TypeScript 2.0版本引入了仅有的尾端类型一never类型。never类型使用never关键字来标识, 不包含任何可能值。示例如下:
function f(): never {
throw new Error();
}
根据尾端类型的定义,never类型是所有其他类型的子类型。所以,never类型允许赋值给任何类型,尽管并不存在never类型的值。
需要注意的是,就算是类型约束最宽松的any类型也不能够赋值给never类型。
应用场景
场景一
never类型可以作为函数的返回值类型,它表示该函数无法返回一个值。我们知道,如果函数体中没有使用return语句,那么在正常执行完函数代码后会返回一个undefined值。在这种情况下,函数的返回值类型是void类型而不是never类型。只有在函数根本无法返回一个值的时候,函数的返回值类型才是never类型。
一种情况就是函数中抛出了异常,这会导致函数终止执行,从而不会返回任何值。在这种情况下,函数的返回值类型为never类型。示例如下:
function throwError(): never {
throw new Error();
//<-该函数永远无法执行到末尾,返回值类型为‘never‘
}
场景二
在“条件类型”中常使用never类型来帮助完成一些类型运算。例如,“Exclude<T,U>”类型是TypeScript内置的工具类型之一,它借助于never类型实现了从类型T中过滤掉类型U的功能。
示例如下:
type T =Exclude<boolean| string, string>; //boolean
场景三
最后一个要介绍的never类型的应用场景与类型推断功能相关。在TypeScript编译器执行类型推断操作时,如果发现已经没有可用的类型,那么推断结果为never类型。示例如下:
function getLength(message: string){
if (typeof message === 'string'){
message; // string
} else{
message; // never
}
}
第5行,在else分支中参数message的类型应该是非string类型。由于函数声明中定义了参数 message的类型是string类型,因此else分支中已经不存在其他可选类型。在这种情况下, TypeScript编译器会将参数message的类型推断为never类型,表示不存在这样的值。
数组类型
TypeScript提供了以下两种方式来定义数组类型:
简便数组类型表示法。
泛型数组类型表示法。
以上两种数组类型定义方式仅在编码风格上有所区别,两者在功能上没有任何差别。
数组类型定义
简便数组类型表示法
const digits: number[]=[0,1,2,3,4,5,6,7,,8,9];
//如果数组中元素是复合类型 使用分组运算符,即小括号
const red:(string|number)[] = ['f','f',0,0,0,0];
泛型数组类型表示法
const digits: Array<number> =[0,1,2,3, 4, 5, 6, 7, 8, 9];
//如果数组中元素是复合类型 不需要使用分组运算符
const red: Array<string|number> = ['f','f',0, O, 0, O];
在定义简单数组类型时,如数组元素为单一原始类型或类型引用,使用简便数组类型表示法更加清晰和简洁。
如果数组元素是复杂类型,如对象类型和联合类型等,则可以选择使用泛型数组类型表示法,它也许能让代码看起来更加整洁一些。
总结起来,目前存在以下三种常见的编码风格供参考:
始终使用简便数组类型表示法。
始终使用泛型数组类型表示法。
当数组元素类型为单一原始类型或类型引用时,始终使用简便数组类型表示法;在其他情况下不做限制。
数组元素类型
在定义了数组类型之后,当访问数组元素时能够获得正确的元素类型信息。示例如下:
const digits:number[] = [0,1,2,3,4,5,6,7,8,9];
const zero = digits[0]
此例中, 虽然没有给常量zero添加类型注解,但是TypeScript编译器能够从数组类型中推断出 zero的类型为number类型。
当访问数组中不存在的元素时将返回undefined值。TypeScript的类型系统无法推断出是否存在数组访问越界的情况,因此即使访问了不存在的数组元素,还是会得到声明的数组元素类型。
只读数组
只读数组与常规数组的区别在于,只读数组仅允许程序读取数组元素而不允许修改数组元素。
使用
ReadonlyArray<T>
内置类型。使用
readonly
修饰符。使用
Readonly<T>
工具类型。
以上三种定义只读数组的方式只是语法不同,它们在功能上没有任何差别。
ReadonlyArray<T>
在TypeScript早期版本中,提供了“ReadonlyArray<T>”类型专门用于定义只读数组。在该类型中,类型参数T表示数组元素的类型。示例如下:
const red:ReadonlyArray<number> = [255,0,0]
readonly
TypeScript 3.4版本中引入了一种新语法, 使用readonly修饰符能够定义只读数组。在定义只读数组时,将readonly修饰符置于数组类型之前即可。示例如下:
const name:readonly Array<string> = ['bob','xox']
Readonly<T>
Readonly<T>是TypeScript提供的一个内置工具类型,用于定义只读对象类型。该工具类型能够将类型参数T的所有属性转换为只读属性,它的定义如下所示:
type Readonly<T> = {
readonly [P in keyof T]:T[P]
};
由于TypeScript 3.4支持了使用readonly修饰符来定义只读数组,所以从TypeScript 3.4开始可以使用Readonly<T>工具类型来定义只读数组。示例如下:
const red:Readonly<number[]> = [255,0,0]
需要注意的是,类型参数T的值为数组类型“number[]”,而不是数组元素类型number。在这一点上,它与ReadonlyArray<T>类型是有区别的。
在进行赋值操作时,允许将常规数组类型赋值给只读数组类型,但是不允许将只读数组类型赋值给常规数组类型。换句话说,不能通过赋值操作来放宽对只读数组的约束。
元组类型
在TypeScript中,元组类型是数组类型的子类型。元组是长度固定的数组,并且元组中每个元素都有确定的类型。
元组定义
我们使用元组来表示二维坐标系中的一个点。该元组中包含两个number类型的元素, 分别表示点的横坐标和纵坐标。示例如下:
//[TO, T1, ...,Tn] 该语法中的T0、T1和Tn表示元组中元素的类型,
const point:[number,number]=[0,0];
const score:[string,number]=['math',100];
元组的值实际上是一个数组,在给元组类型赋值时,数组中每个元素的类型都要与元组类型的定义保持兼容。例如,对于“[number,number]”类型的元组,它只接受包含两个number类型元素的数组。
若数组元素的类型与元组类型的定义不匹配,则会产生编译错误。
在给元组类型赋值时,还要保证数组中元素的数量与元组类型定义中元素的数量保持一致,否则将产生编译错误。
只读元组
元组可以定义为只读元组,这与只读数组是类似的。只读元组类型是只读数组类型的子类型。
定义只读元组有以下两种方式:
使用readonly修饰符。
使用“Readonly<T>”工具类型。
以上两种定义只读元组的方式只是语法不同,它们在功能上没有任何差别。
readonly
TypeScript3.4版本中引入了一种新语法,使用readonly修饰符能够定义只读元组。在定义只读元组时,将readonly修饰符置于元组类型之前即可。示例如下:
const arr:readonly [number,number] = [0,0];
Readonly<T>
由于TypeScript 3.4支持了使用readonly修饰符来定义只读元组,所以从TypeScript 3.4开始可以使用“Readonly<T>”工具类型来定义只读元组。示例如下:
const arr:Readonly<[number,number] = [0,1];
在给只读元组类型赋值时,允许将常规元组类型赋值给只读元组类型,但是不允许将只读元组类型赋值给常规元组类型。
访问元组中的元素
由于元组在本质上是数组,所以我们可以使用访问数组元素的方法去访问元组中的元素。在访问元组中指定位置上的元素时,编译器能够推断出相应的元素类型。示例如下:
const score:[string,number] = ['math',1000];
const course = score[0]; //string
const grade = score[1] //number
当访问数组中不存在的元素时不会产生编译错误。与之不同的是,当访问元组中不存在的元素时会产生编译错误。
修改元组元素值的方法与修改数组元素值的方法相同。
元组类型中的可选元素
在定义元组时,可以将某些元素定义为可选元素。定义元组可选元素的语法是在元素类型之后添加一个问号“?”,具体语法如下所示:
[TO?, T1?, ...,Tn?]
[T0,T1?, ..., Tn?] //如果元组中同时存在可选元素和必选元素,那么可选元素必须位于必选元素之后
下例中定义了一个包含三个元素的元组tuple,其中第一个元素是必选元素,后两个元素是可选
const arr:[boolean,string?,number?] = [true,'yes',1];
const arr:[boolean,string?,number?] = [true];//在给元组赋值时,可以不给元组的可选元素赋值。
元组类型中的剩余元素
在定义元组类型时,可以将最后一个元素定义为剩余元素。
如果元组类型的定义中含有剩余元素,那么该元组的元素数量是开放的,它可以包含零个或多个指定类型的剩余元素。示例如下:
const tuple:[number...string[]] = [0,'a','b'];
let tuple:[number,...string[]];
tuple = [0];
tuple = [0,'a'];
tuple = [0,'a','b'];
tuple = [0,'a','b','c'];
tuple = [0,'a','b','c','d'];
元组的长度
对于经典的元组类型, 即不包含可选元素和剩余元素的元组而言,元组中元素的数量是固定的。
也就是说,元组拥有一个固定的长度。TypeScript编译器能够识别出元组的长度并充分利用该信息来进行类型检查。示例如下:
function f(point:[number,number]){
// 编译器推断出length的类型为数字字面量类型2
const length = point.length;
if(length === 3){//编译错误!条件表达式永远为false
//...
}
}
当元组中包含了可选元素时,元组的长度不再是一个固定值。编译器能够根据元组可选元素的数量识别出元组所有可能的长度,进而构造出一个由数字字面量类型构成的联合类型来表示元组的长度。示例如下:
const tuple:[boolean,string?,number?] = [true,'yese',1];
let len = tuple.length; //联合类型 1|2|3 ,值是3
len = 1;
len = 2;
len = 3;
len = 4; //编译错误!类型'4'不能赋值给类型'1|2|3'
若元组类型中定义了剩余元素,那么该元组拥有不定数量的元素。因此,该元组length属性的类型将放宽为number类型。示例如下:
const tuple:[number,...string[]] = [0,'a'];
const len = typle.length; //number
元组类型与数组类型的兼容性
前文提到过,元组类型是数组类型的子类型, 只读元组类型是只读数组类型的子类型。 在进行赋值操作时,允许将元组类型赋值给类型兼容的元组类型和数组类型。示例如下:
const point1: [number,number] = [1,1];
const point2: [number,...number[]] = [1,1,1,1,1,1];
const point3: [number,number?] = [1];
const nums1:number[] = point1;
const nums2:number[] = point2;
const nums3:number[] = point3;//编译错误
// 不能将类型“[number, (number | undefined)?]”分配给类型“number[]”。
// 不能将类型“number | undefined”分配给类型“number”。ts(2322)
元组类型允许赋值给常规数组类型和只读数组类型,但只读元组类型只允许赋值给只读数组类型。
const t:[number,number]=[0,0];
const rt:readonly[number, number] = [0, 0];
let a:number[] = t;
let ra:readonly number[];
ra = t;
ra = rt;
由于数组类型是元组类型的父类型,因此不允许将数组类型赋值给元组类型。
对象类型
在JavaScript中存在这样一种说法,那就是“一切皆为对象”。有这种说法是因为JavaScript中的绝大多数值都可以使用对象来表示。例如,函数、数组和对象字面量等本质上都是对象。对于原始数据类型,如string类型,JavaScript也提供了相应的构造函数new String('hi')来创建能够表示原始值的对象。
在某些操作中,原始值还会自动地执行封箱操作,将原始数据类型转换为对象数据类型。
let a = 'hi'.toUpperCase()
console.log(typeof a); //string
console.log(3..toString());
前面已经介绍过的数组类型、元组类型以及后面章节中将介绍的函数类型、接口等都属于对象类型。由于对象类型的应用非常广泛,因此TypeScript提供了多种定义对象类型的方式。在本节中,我们将首先介绍三种基本的对象类型:
Object类型(首字母为大写字母O)
object类型(首字母为小写字母o)
对象类型字面量
Object
这里的Object指的是Object类型,而不是JavaScript内置的“Object()”构造函数。Object类型表示一种类型,而“Object()”构造函数则表示一个值。因为“Object()”构造函数是一个值,因此它也有自己的类型。
const obj333 = new Object();
console.log(typeof obj333); //object
//深入分析一下TypeScript源码中对“Object()”构造函数的类型定义。
interface ObjectConstructor {
readonly prototype: Object;
// 省略了其他成员
}
declare var Object: ObjectConstructor;
现在,我们可以正式地引出Object类型。Object类型是特殊对象“Object.prototype”的类型, 该类型的主要作用是描述JavaScript中几乎所有对象都共享(通过原型继承)的属性和方法。
由该定义能够直观地了解到“Object()”构造函数的类型是ObjectConstructor类型而不是Object类型, 它们是不同的类型。第7行,prototype属性的类型为Object类型。构造函数的prototype属性值决定了实例对象的原型。此外,“Object.prototype”是一个特殊的对象,它是JavaScript中的公共原型对象。
类型兼容性
Object类型有一个特点,那就是除了undefined值和nul值外,其他任何值都可以赋值给Object 类型。示例如下:
let obj:Object;
obj = {x:0};
obj = true;
obj = 'hi';
obj = 1;
//编译错误
obj = undefined;
obj = null;
对象能够赋值给Object类型是理所当然的,但为什么原始值也同样能够赋值给Object类型呢?
实际上,这样设计正是为了遵循JavaScript语言的现有行为。JavaScript语言中存在自动封箱操作。当在原始值上调用某个方法时,JavaScript会对原始值执行封箱操作,将其转换为对象类型,然后再调用相应方法。Object类型描述了所有对象共享的属性和方法,而 JavaScript允许在原始值上直接访问这些方法,因此TypeScript允许将原始值赋值给Object类型。
常见错误
在使用Object类型时容易出现的一个错误是,将Object类型应用于自定义变量、参数或属性等的类型。示例如下:
const point:Object = {x:0,y:0};
此例中,将常量point的类型定义为Object类型。虽然该代码不会产生任何编译错误,但它是一个明显的使用错误。原因刚刚介绍过,Object类型的用途是描述“Object.prototype”对象的类型,即所有对象共享的属性和方法。在描述自定义对象类型时有很多更好的选择,完全不需要使用Object 类型,例如接下来要介绍的object类型和对象字面量类型等。在TypeScript官方文档中也明确地指出了不应该使用Object类型,而是应该使用object类型来代替。
object
在TypeScript 2.2版本中,增加了一个新的object类型表示非原始类型。object类型使用object 关键字作为标识,object类型名中的字母全部为小写。示例如下:
const point:object = {x:0,y:0};
object类型的关注点在于类型的分类,它强调一个类型是非原始类型,即对象类型。object类型的关注点不是该对象类型具体包含了哪些属性,例如对象类型是否包含一个名为name的属性,因此,不允许读取和修改object类型上的自定义属性。示例如下:
const obj: object ={foo:0 };
//编译错误!属性‘foo‘不存在于类型‘object‘上 obj。foo;
obj.foo;
//编译错误!属性‘foo‘不存在于类型‘object‘上 obj。foo = 0;
obj.foo = 0;
在object类型上仅允许访问对象的公共属性和方法, 也就是Object类型中定义的属性和方法。
const obj:object = {};
obj.toString();
obj.value0f();
在 TypeScript 中,object
和 Object
两种类型实际上有着一些区别。
object
类型是 TypeScript 2.2 版本之后引入的一个新类型,表示非原始类型,也就是除了number
,string
,boolean
,symbol
,null
,或undefined
之外的类型。所以,一个object
类型的变量可以指向任何非原始类型的值,例如数组、函数、日期、正则等。Object
类型则是 JavaScript 中全局的 Object 构造函数,使用Object
类型意味着一个值可以是任何类型的对象。由于 JavaScript 中几乎所有的值都是对象,因此Object
类型可以包括几乎所有的 JavaScript 值,包括原始类型的值。
在 TypeScript 中,为了更精确地描述对象的类型,我们通常使用接口(interface)或者类型别名(type alias)来定义具体的形状(shape)。例如:
interface Person {
name: string;
age: number;
}
// 或者
type Person = {
name: string;
age: number;
};
然后我们可以使用这些类型来声明对象:
let john: Person = { name: 'John', age: 30 };
总的来说,如果你想要描述一个任意的非原始类型的值,你可能会使用 object
类型。而如果你想要描述一个可以是任何类型的值,你可能会使用 Object
类型。然而,为了更精确和有用的类型检查,最好使用接口或类型别名来描述具体的对象类型。
object类型仅能够赋值给以下三种类型:
顶端类型any和unknown。
Object类型。
空对象类型字面量“{}”。
对象类型字面量
使用对象字面量来创建一个对象类型。对象字面量可以让你精确地指定一个对象的结构和类型。这对于保持代码的类型安全性非常有用。
以下是一个对象字面量类型的例子:
let person: { name: string; age: number; } = {
name: 'John',
age: 30
};
在这个例子中,person
被定义为一个对象类型,这个对象有两个属性:name
和 age
。name
的类型是 string
,age
的类型是 number
。
你可以在对象字面量类型中定义任意数量的属性,并为每个属性指定类型。
对象字面量类型也可以包含方法:
let person: {
name: string;
age: number;
sayHello: () => string;
} = {
name: 'John',
age: 30,
sayHello: () => 'Hello, ' + person.name
};
在这个例子中,sayHello
是一个方法,它的类型是一个没有参数并返回字符串的函数。
对象类型字面量的类型成员可分为以下五类:
属性签名
调用签名
构造签名
方法签名
索引签名
属性签名
let point:{x:number,y:number}={x:0,y:0};
属性签名中的属性名可以为可计算属性名,但需要该可计算属性名满足以下条件之一:
- 可计算属性名的类型为string字面量类型或number字面量类型。
const a: 'a' = 'a';
const b: 0 = 0;
let obj123: {
[a]: boolean;
[b]: boolean;
};
obj123 = { 'a': false, 0: true};
- 可计算属性名的类型为“unique symbol”类型。
const s: unique symbol = Symbol();
let obj321: {
[s]: boolean;
};
- 可计算属性名符合“Symbol.xxx”的形式。
let myObject: {
[Symbol.toStringTag]: string;
};
在属性签名的语法中,表示类型的Type部分是可以省略的,允许只列出属性名而不定义任何类型。在这种情况下,该属性的类型默认为any类型。示例如下:
let bbb:{
x;
y;
}
//等同于
let aaa:{
x: any;
y: any;
}
ts配置文件需要配置不检查对象属性隐式获得any类型, "noImplicitAny": false
可选属性
默认情况下,通过属性签名定义的对象属性是必选属性,如在属性签名中的属性名后添加一个?,那么将定义一个可选属性
let point:{x:number;y:number;z?:number};
point = {x:0,y:0};
point = {x:0,y:0,z:0};
在ts配置严格检查null模式下(strictNullChecks = true),可选属性可以传undefined,但不能传null
非严格检查模式,可选属性null和undefined都可以传
let obj: {
x: number;
y: number;
z?: number;
};
// 等同于
let obj2: {
x: number;
y: number;
z?: number | undefined;
};
obj = { x: 0, y: 0, z: undefined };
只读属性
在属性签名定义中添加readonly修饰符能够定义对象只读属性,只读属性不允许再被修改。
let obj: {
readonly x: number;
readonly y: number;
z?: number;
};
// 初始化
obj = { x: 0, y: 0, z: undefined };
obj.x = 1; //编译错误 无法为“x”赋值,因为它是只读属性。ts(2540)
空对象类型字面量
空对象类型字面量表示不带有任何属性的对象类型,因此不允许在“{}”类型上访问任何自定义属性。
只允许访问对象的公共属性和方法,也就是Object类型上的定义的方法和属性。
let obj: {} = {x:0,y:0};
obj.x; //编译错误 类型“{}”上不存在属性“x”。ts(2339)
console.log(obj.valueOf());
两者的区别主要在于语义上。全局的Object类型用于描述对象公共的属性和方法,它相当于一种专用类型,因此程序中不应该将自定义变量、参数等类型直接声明为Object类型。空对象类型字面量“{}”强调的是不包含属性的对象类型,同时也可以作为Object类型的代理来使用。最后,也要注意在某些场景中新的object类型可能是更加合适的选择。
弱类型
弱类型(WeakType)是TypeScript 2.4版本中引入的一个概念。弱类型指的是同时满足以下条件的对象类型:
对象类型中至少包含一个属性。
对象类型中所有属性都是可选属性。
对象类型中不包含字符串索引签名、数值索引签名、调用签名和构造签名
let config:{
url?:string;
async?:boolean;
timeout?:number;
}
多余属性
对象多余属性可简单理解为多出来的属性。
假设存在源对象类型和目标对象类型两个对象类型,那么当满足以下条件时,我们说源对象类型相对于目标对象类型存在多余属性,具体条件如下:
源对象类型是一个“全新(Fresh)的对象字面量类型”。
源对象类型中存在一个或多个在目标对象类型中不存在的属性。
“全新的对象字面量类型”指的是由对象字面量推断出的类型。
多余属性检查
将包含多余属性的对象字面量赋值给类型为“{x:number;y:number}”的point常量后,程序中就再也无法引用对象字面量“{x:0,y:0,z:0}”的类型了。从类型系统的角度来看,该赋值操作造成了类型信息的永久性丢失,因此编译器认为这是一个错误。
多余属性检查能够带来的最直接的帮助是发现属性名的拼写错误
允许多余属性
多余属性检查在绝大多数场景中都是合理的,因此推荐在程序中尽可能地利用这个功能。但如果确定不想让编译器对代码进行多余属性检查,那么有多种方式能够实现这个效果。
- 使用类型断言(推荐)
const p0: {x: number}={x: 0,y: 0} as {x: number};
- 启用“suppressExcessPropertyErrors”编译选项
启用该编译选项能够完全禁用整个TypeScript工程的多余属性检查,但同时也将完全失去多余属性检查带来的帮助。我们可以在tsconfig.json配置文件中或命令行上启用该编译选项。
- 使用“// @ts-ignore”注释指令
// @ts-ignore
const point:{x: number}={x:0,y: 0};
该注释指令能够禁用针对某一行代码的类型检查。
- 为目标对象类型添加索引签名
const point: {
x: number;
[prop:string]: number;//索引签名 }= {x:0,y:0};
}
若目标对象类型上存在索引签名,那么目标对象可以接受任意属性,因此也就谈不上多余属性。
- 最后一种方法也许不是很好理解。如果我们先将对象字面量赋值给某个变量,然后再将该变量赋值给目标对象类型,那么将不会执行多余属性检查。这种方法能够生效的原理与类型断言类似,那就是令源对象类型不为“全新的对象字面量类型”,于是编译器将不执行多余属性检查。
const temp={ x:0,y:0 };
// 无编译错误
const point: { x: number } = temp;
函数类型
参数类型、返回值类型、this类型以及函数重载等
常规参数类型
在函数形式参数列表中,为参数添加类型注解就能够定义参数的类型。针对函数表达式和匿名函数,也一样,如果函数形式参数列表中没有明确指定参数类型,那么参数类型将默认any类型,隐式获得any类型,注意ts配置 noimplicitAny
function add(x:number,y:number){
return x+y;
}
const f = function(x:number,y:number){
return x+y;
}
可选参数类型
js中,函数的每个参数都是可选参数,而在ts中,默认情况函数的每一个参数都是必选参数,如需可选参数,则需要再函数类型定义中明确指定。
函数的可选参数必须位于函数参数列表的末尾位置。
调用函数时,不能少传也不能多传
ts配置strictNullChecks模式下,ts自动为可选参数添加undefined类型,允许给可选参数传入undefined
function add(x:number,y?:number){
return x + (y ?? 0);
}
add(); //编译错误
add(1);// 正确
add(1,2);// 正确
add(1,2,3);//编译错误
默认参数类型
函数默认参数类型可以通过类型注解定义,也可以根据默认参数值自动地推断类型。
如果函数定义了默认参数,并且默认参数处于函数参数列表末尾的位置,该参数将被视为可选参数,可以不传值
如果默认参数之后存在必选参数,那么该默认参数不是可选参数,必须传值
同一函数参数不允许同时声明为可选参数和默认参数
function add(x:number,y:number = 0){
return x+y;
}
add(1)
add(1,2)
剩余参数类型
必须参数、可选参数和默认参数处理的都是单个参数,而剩余参数处理的则是多个参数。如果函数定义中声明了剩余参数,那么调用函数时会将多余的实际参数收集到剩余参数列表中(数组类型或元组类型)。
数组类型的剩余参数
function f(...args:number[]){}
f();
f(0);
f(0,1)
元组类型的剩余参数
function f0(...args:[boolean,number]){}
//等同于
function f1(args_0:boolean,args_1:number){}
如果剩余参数的类型为元组类型,编译器会将剩余参数展开为独立的形式参数声明:
- 常规元组类型
function f0(...args:[boolean,number]){}
//等同于
function f1(args_0:boolean,args_1:number){}
- 带有可选的元组类型
function f0(...args:[booleand,string?]){}
//等同于
function f1(args_0:boolean,args_1?:string){}
- 带有剩余元素的元组类型
function f0(...args:[boolean,...string[]]){}
//等同于
function f1(args_0:boolean,...args_1:string[]){}
了解元组类型剩余参数的展开行为后,也就清楚了该如何传入对应的实际参数
function f0(...args: [boolean, number, string]) {}
f0(true, 1, '');
function f1(...args: [boolean, number, string?]) {}
f1(false, 1);
f1(false, 1, '');
function f2(...args: [boolean, number, ...string[]]) {}
f2(false, 1);
f2(false, 1, '');
f2(false, 1, '', 'hi');
function f3(...args:[boolean,number?,...string[]]){}
f3(false)
f3(false,1)
f3(false,1,'','hi')
解构参数类型
前面介绍了如何对数组和对象进行解构,解构还可以应用在函数参数列表中。
function f0([x,y]){}
f0([0,1]);
function f1({x,y}){}
f0({x:0,y:1});
//可以使用类型注解为解构参数添加类型信息
function f0([x,y]:[number,number]){}
f0([0,1])
function f1({x,y}:{x:number;y:number}){}
f1({x:0,y:1})
返回值类型
在函数形式参数列表之后,可以使用类型注解为函数添加返回值类型。
绝大多数情况下,ts能够根据函数体内的return语句等自动推断出返回值类型,因此也可以省略返回值类型
function add(x:number,y:number):number{
return x + y;
}
function add(x:number,y:number){
return x + y;
}
在ts的原始类型里有一个特殊的空类型void,唯一的使用场景就是作为函数的返回值类型,和java一样代表该函数没有返回值,只不过ts会默认返回undefined
function f0():void{
return undefined;
}
function f1():void{} //默认返回undefined;
如果没有启用strictNullChecks ,那么void返回值类型也允许返回null。
函数类型字面量
函数类型字面量是定义函数类型的方法之一,它能够指定函数的参数类型、返回值类型以及泛型类型参数。
// 声明一个函数类型f,该函数必须无参,无返回值
let f:()=>void;
//定义函数
f = function(){};
在函数类型字面量中定义函数参数类型时,必须包含形式参数名,不允许只声明参数的类型,函数类型字面量中的形式参数名与实际函数值的形式参数名不必相同。
函数类型字面量中的返回值类型必须明确指定,不允许省略,没有返回值则使用void类型作为返回值类型
let add:(x:number,y:number) => number; //必须包含形式参数名
// 函数类型字面量中声明形式参数名为x,实际函数值的形式参数名为y,可以不必相同
let f:(x:number)=>number;
f = function(y:number):number{
return y;
}
调用签名
函数本质上是一个对象,但特殊的地方在于函数是可调用的对象。因此,可以使用对象类型来表示函数类型。
在编程中,我们通常把函数的参数列表和返回值类型称为函数的 "签名" 或 "接口"。这是因为这些元素定义了如何与函数交互:它们告诉我们函数接受什么样的输入(参数列表),以及返回什么样的输出(返回值类型)。
在 TypeScript 中,"调用签名" 就是这样一个定义,它描述了函数的参数列表和返回值类型。之所以叫做 "调用签名",是因为它定义了如何 "调用" 函数。也就是说,调用签名告诉我们怎样以正确的方式调用一个函数。
例如,假设我们有这样一个调用签名:
(param1: number, param2: string) => boolean
这个调用签名告诉我们,这个函数接受两个参数,一个是 number
类型,另一个是 string
类型,并且返回一个 boolean
类型的值。
因此,当你看到这个调用签名时,你就知道了如何正确地调用这个函数:你需要提供一个 number
和一个 string
,然后你可以期待得到一个 boolean
。
let add:(param1: number, param2: string) => boolean;// 函数类型字面量
let add1:{(param1:number,param2:string):string;};//对象类型
构造函数类型字面量
在面向对象编程中,构造函数是一类特殊的函数,用来创建和初始化对象。js中的函数可以作为构造函数使用,在调用构造函数时需要使用new运算符。
构造函数类型字面量是定义构造函数类型的方法之一,能指定构造函数的参数类型、返回值类型以及泛型类型参数。
js提供了内置Error构造函数,接受一个可选的message作为参数并返回新创建的Error对象。
const a = new Error();
const b = new Error('Error message')
//我们可以使用如下构造函数字面量来表示Error构造函数的类型。
let ErrorConstructor:new (message?:string) => Error;
构造签名
与调用签名类型。若在对象类型中定义了构造签名类型成员,那么我们称该对象类型为构造函数类型
在该语法中,new是运算符关键字,ParameterList表示构造函数形式参数列表类型,Type表示构造函数返回值类型,两者都是可选的。
let Dog:{new (name:string):object};
// 构造函数类型字面量简写
let Dog: new (name:string) => object
Dog = class{
private name:string;
constructor(name:string){
this.name = name;
}
};
let dog = new Dog('wangwang')
调用签名与构造签名
有一些函数被设计为既可以作为普通函数使用,同时又可以作为构造函数来使用。例如 JavaScript内置的“Number()”函数和“String()”函数等都属于这类函数。示例如下:
const a:number = Number(1);
const b:Number = new Number(1);
//若在对象类型中同时定义调用签名和构造签名,则能够表示既可以被直接调用,又可以作为构造函数使用的函数类型。示例如下:
{
new (x:number):Number;// 构造签名
(x:number):number; // 调用签名
}
declare const F:{
new (x:number):Number;
(x:number):number;
}
const a:number = F(1); //普通函数调用
const b:Number = new F(1); //构造函数调用
重载函数
函数重载
不带函数体的函数声明语句叫做函数重载,它只提供了函数的类型信息。
函数重载允许存在一个或多个,但只有多于一个的函数重载才有意义,因为若只有一个函数重载,则可以直接定义函数实现。在函数重载中,不允许使用默认参数。同名不同参
function add(x:number,y:number):number;
function add(x:any[],y:any[]):any[];
函数实现
函数实现包含了实际的函数体代码,编译时存在,编译生成的js代码同样存在。
每一个重载函数只允许有一个函数数显,并且它必须位于所有函数重载语句之后
function add(x:number,y:number):number;
function add(x:any[],y:any[]):any[];
function add(x:number|any[],y:number|any[]):any{
// ...
}
//函数实现需要兼容每个函数重载中的函数签名,函数实现的函数签名类型必须能够赋值给函数重载的函数签名类型。
function foo(x:number):boolean;
function f00(x:string):void;
function foo(x:number|string):any{
//...
}
//此例中,重载函数foo可能的参数类型为number类型或string类型,同时返回值类型可能为 boolean类型或void类型。因此,在函数实现中的参数x必须同时兼容number类型和string类型,而返回值类型则需要兼容boolean类型和void类型。我们可以使用联合类型来解决这些问题,示例如
其它编程语言中允许存在多个函数实现,并且在调用重载函数时编程语言负责选择合适的函数实现执行,ts中,重载函数只存在一个函数实现,开发者需要再这个唯一的函数实现中实现所有函数重载功能,需要自行检测参数的类型及数量,根据判断结果去执行不同的操作
function add(x:number,y:number):number;
function add(x:any[],y:any[]):any[];
function add(x:number|any[],y:number|any[]):any{
if(typeof x === 'number' && typeof y === 'number'){
return x + y;
}
if (Array.isArray(x) && Array.isArray(y)){
return [...x,...y];
}
}
TypeScript不支持为不同的函数重载分别定义不同的函数实现。从这点上来看, TypeScript中的函数重载不是特别便利。
函数重载解析顺序
许多场景并不需要声明重载函数,用到时再了解
函数中的this值的类型
默认情况下,编译器会将函数中的this值设置为any类型,并允许程序在this值上执行任意操作。
ts提供了一个配置 noImplicitThis 编译选项,当启用了该配置,如果this值默认获得了any类型,编译将报错
函数的this参数
TypeScript支持在函数形式参数列表中定义一个特殊的this参数来描述该函数中this值的类型。
function foo(this:{name:string}){
this.name = 'Bob';
this.name = 0; //编译错误
}
this参数固定使用this作为参数名。
this参数是一个可选的参数,若存在,则必须作为函数形式参数列表中的第一个参数。
this参数的类型即为函数体中this值的类型。
this参数不同于常规的函数形式参数,只存在编译阶段,运行时的代码(编译生成的js代码)不存在这个this参数
如果我们想要定义一个纯函数或者是不想让函数代码依赖于this的值,那么在这种情况下可以明确地将this参数定义为void类型。这样做之后,在函数体中就不允许读写this的属性和方法。
接口
类似于对象类型字面量,接口类型也能够表示任意的对象类型。不同的是,接口类型能够给对象类型命名以及定义类型参数。接口类型无法表示原始类型,如boolean类型等。
接口声明只存在于编译阶段,在编译后生成的JavaScript代码中不包含任何接口代码。
接口声明
通过接口声明能够定义一个接口类型。
interface Shape{}
语法角度看,接口声明就是在对象类型字面量之前添加了interface关键字和接口名
同样的,接口类型的类型成员也分为以下五类
- 属性签名
- 调用签名
- 构造签名
- 方法签名
- 索引签名
属性签名
属性签名声明了对象类型中属性成员的名称和类型。
PropertyName:Type;
//该语法中,PropertyName表示对象属性名,可为标识符、字符串、数字和可计算属性名
interface Point{
x:number;
y:number;
}
调用签名
调用签名定义了该对象类型表示的函数在调用时的类型参数、参数列表以及返回值类型
(ParameterList):Type
//ParameterList表示函数形式参数列表类型;Type表示函数返回值类型,两者都是可选的
interface ErrorConstructor{
(message?:string):Error;
}
构造签名
构造签名定义了该对象类型表示的构造函数在使用new运算符调用时的参数列表以及返回值类型
new (ParameterList):Type
// new是运算符关键字,parameterList表示构造函数形式参数列表类型,Type表示构造函数返回值类型,两者都是可选的
interface ErrorConstructor{
new (message?:string): Error;
}
方法签名
方法签名是声明函数类型的属性成员的简写
PropertyName(ParamaterList):Type
// PropertyName表示对象属性名,可为标识符、字符串、数字和可计算属性名
// paramsterList表示可选的方法形式参数列表类型
// Type表示可选的方法返回值类型
// 语法角度看,方法前面是在调用签名之前添加一个属性名作为方法名
interface Document{
getElementByid(elementId:string):HTMLElement|null;
}
之所以说方法签名是声明函数类型的属性成员的简写,是因为方法签名可以改写为具有同等效果但语法稍显复杂的属性签名。
interface A {
f(x: boolean): string; //方法签名
}
interface B {
f: { (x: boolean): string }; //属性签名和对象类型字面量
}
interface C {
f: (x: boolean) => string; //属性签名和函数类型字面量
}
方法签名中的属性名可以为可计算属性名,这一点与属性签名的规则是相同的。
const f = 'f';
interface D {
[f]: number;
}
若接口包含多个名字相同参数列表不同的方法签名成名,则表示该方法是重载方法,它具有三种调用签名
interface E{
f():number;
f(x:boolean):boolean;
f(x:string,y:string):string;
}
索引签名
js支持使用索引访问对象的属性,即通过"[]"语法去访问对象属性
一个典型的例子是数组对象,我们既可以使用数组所有去访问数组元素,也可以使用字符串索引去访问数组对象上的属性和方法
const colors = ['red','green','blue'];
const red = colors[0];
const len = colors['length']
接口中的索引签名能够描述使用索引访问的对象属性的类型,索引签名只有以下两种
- 字符串索引签名
- 数值索引签名
字符串索引签名
IndexName表示索引名,它可以为任意合法的标识符,索引名只起到占位作用,不代表真实的对象属性名,字符串索引签名中,索引名的类型必须为string类型,type表示索引值的类型,可以为任意类型
[IndexName:string]:Type
interface A{
[prop:string]:number;
}
一个接口中最多只能定义一个字符串索引签名,字符串索引签名会约束该对象类型中所有属性的类型,该接口中所有属性的类型必须能够赋值给number类型
interface A{
[prop:string]:number;
a:number;
b:0;
c:1|2
}
数值索引签名
IndexName表示索引名,它可以为任意合法的标识符,索引名只起到占位作用,不代表真实的对象属性名,数值索引签名中,索引名的类型必须为number类型,type表示索引值的类型,可以为任意类型
[IndexName:number]:Type
interface A{
[prop:number]:string;
}
//一个接口最多只能定义一个数值索引签名。
interface A{
[prop:number]:string;
}
const onj:A=['a','b','c'];
obj[0];//string
若接口中同时存在字符串索引签名和数值索引签名,那么数值索引签名的类型必须能够赋值给字符串索引签名的类型。因为在JavaScript中,对象的属性名只能为字符串(或Symbol)。虽然js允许使用数字等其他值作为对象的所有,但最总它们都会被转换为字符串类型。
因此,数值索引类型签名能够表示的属性集合是字符串索引签名能够表示的属性集合的子集。
//字符串索引签名的类型为number类型,数值索引签名的类型为数字字面量联合类型“0 |1”。由于“0|1”类型能够赋值给number类型,因此该接口定义是正确的。示例如下:
interface A{
[prop:string]:number | string;
[prop:number]:0|1; //必须是字符串索引签名的属性集合的子集
}
//但如果我们交换字符串索引签名和数值索引签名的类型,则会产生编译错误。
interface A{
[prop:string]:0|1;
[prop:number]:number; //编译错误
}
可选属性与方法
默认情况 接口中属性签名和方法签名定义的对象属性都是必选的。
可以在属性名或方法名后添加一个?,从而将该属性或方法定义为可选的。
interface Y {
x?: string;
y?(): number;
}
const test:Y = {};
const test1:Y = {x:'test'};
const test2:Y = {y() {return 0;}};
const test3:Y = {x:'test',y(){return 0;}};
如果接口中定义了重载方法,那么所有重载方法签名必须同时为必选或者可选的
只读属性与方法
接口声明中,使用readonly修饰符能够定义只读属性,只允许在属性签名和索引签名中使用
interface A{
readonly a:string;
readonly [prop:string]:string;
readonly [prop:number]:string;
}
若接口中定义了只读索引签名,那么接口类型中的所有属性都是只读属性
interface A{
readonly [prop:string]:number;
}
const a:A = {x:0};
a.x = 1 //编译错误,不允许修改属性值
如果接口中既定义了只读索引签名,又定义了非只读的属性签名,那么非只读的属性签名定义的属性依旧是非只读的,除此之外的所有属性都是只读的。
接口的继承
接口可以继承其他的对象类型,这相当于将继承的对象类型中的类型成员复制到当前接口中,接口可以继承的对象类型如下:
- 接口
- 对象类型的类型别名
- 类
- 对象类型的交叉类型
接口的继承需要使用extends关键字
interface AA {
name: string;
}
interface BB extends AA {
age: number;
}
注意
当一个接口继承了其他接口,子接口既包含了自身定义的类型成员,也包含了父接口中的类型成员。
如果子接口与父接口之间存在同名的类型成员,那么子接口中的类型成员具有更高的优先级。
子接口与父接口中的同名类型成员必须是类型兼容的。也就是说,子接口中同名类型成员的类型需要能够赋值给父接口中同名类型成员的类型,否则将产生编译错误。
如果仅是多个父接口之间存在同名的类型成员,而子接口本身没有该同名类型成员,那么父接口中同名类型成员的类型必须是完全相同的,否则将产生编译错误。解决这个问题的一个办法是,在子接口覆盖父接口,子接口则具更高优先级
类型别名
如同接口声明能够为对象类型命名,类型别名则能够为ts中的任意类型命名
类型别名声明
类型别名声明能够定义一个类型别名
type AliasName = Type
// type是声明类型别名的关键字;
// aliasName表示类型别名的名称
// Type表示类型别名关联的具体类型
类型别名的名称必须为合法的标识符。由于类型别名表示一种类型,因此类型别名的首字母通常需要大写。
类型别名引用的类型可以为任意类型,例如原始类型、对象类型、联合类型、交叉类型等。也可以引用其他类型别名
type StringType = string;
type BooleanType = true|false;
type point = {x:number;y:number,z?:number};
type Numeric = number | bigint;
type StringOrNumber = string | Numeric;
类型别名不会创建出一种新的类型,它只是给已有类型命名并直接引用该类型。
使用类型别名与直接使用该类型别名引用的类型是完全等价的。可能会有一些比较复杂的或者书写起来比较长的类型,这是我们就可以声明一个类型别名来引用该类型,也便于我们对这个类型进行重用。
type Point = {x:number,y:number};
let a:Point;
// let a:{x:number;y:number}
递归的类型别名
ts3.7之前 类型别名声明中赋值运算符右侧的类型不允许引用当前定义的类型别名,因为类型别名对齐引用的类型使用的是及早求值的策略。
ts3.7后,编译器对类型别名的解析进行了一些优化,使用惰性求值策略解析泛型类型参数,因此允许在泛型类型参数中递归地使用类型别名
1)若类型别名引用的类型为接口类型、对象类型字面量、函数类型字面量和构造函数类型字面量,则允许递归引用类型别名
type T0 = {name:T0};
type T1 = () => T1;
type T2 = new ()=>T2;
2)若类型别名引用的是数组类型或元组类型,则允许在元素类型中递归地用类型别名。
type T0= Array<T0>;
type T1 = T1[];
type T3 = [number,T3];
3)若类型别名引用的是泛型类或泛型接口,则允许在类型参数中递归的引用类型别名。
interface A<T>{
name:T;
}
type T0 = A<T0>;
class B<T>{
name:T|undefined;
}
type T1 = B<T1>;
通过递归的类型别名能够定义一些特别常用的类型。ts官方给出了使用递归的类型别名来定义json类型的例子
type Json =
| string
| number
| boolean
| null
| {[peoperty:string]:Json}
| Json[];
const data:Json = {
name:'TypeScript',
version:{major:3}
}
类型别名与接口
类型别名与接口相似,他们都可以给类型命名并通过该名字来引用表示的类型,类型别名和接口还是存在一些差别
- 类型别名能够表示非对象类型,而接口只能表示对象类型
- 接口可以继承其他的接口、类等对象类型,而类型别名则不支持继承
- 接口名总是会显示在编译器的诊断信息和代码编辑器的智能提示信息中,而类型别名的名字只在特定情况下才会显示出来
- 接口具有声明合并的行为,而类型别名则不会进行声明合并。
类
类的定义
定义一个类需要使用class关键字。类似于函数定义,类的定义也有以下两种方式:
类声明
类表达式
类声明
class Circle {
redius:number;
}
const c = new Circle();
类表达式
下例中使用了类表达式定义了一个匿名类,同时使用常量Circle引用了该匿名类
const Circle = class {
redius:number;
}
const a = new Circle();
//如果在类表达式中定义类名,则该类名只能够在类内部使用,在类外不允许引用该类名
const A = class B{
name = B.name;
}
const b = new B()//错误
成员变量
Circle类只包含了一个成员变量。其中,radius是成员变量名,成员变量名之后的类型注解定义了该成员的变量的类型,除了在成员变量声明中设置了初始值,我们还可以在类的构造函数中设置成员变量的初始值
class Circle {
radius:number;
constructor(){
this.radius = 1;
}
}
--strictPropertyInitialization
虽然类的成员变量设置初始值是可选的,但是对成员变量进行初始化是一个好的编程实践,它能够有效避免使用未初始化的值而引发错误。因此,ts提供了配置来帮助严格检查未初始化的成员变量,strictPropertyInitialization 编译选项必须与 strictNullChecks编译选项同时启用,否则不起作用
一些场景中,我们需要通过调用某些方法来初始化类的成员变量,可以使用非空类型断言“!”来通知编译器该成员变量已经进行初始化,以避免产生变异错误
readonly属性
声明类的成员变量时,在成员变量之前添加readonly修饰符,将该成员变量声明为只读,只读成员变量必须在声明时初始化或在构造函数里初始化
class A{
readonly a = 0;
readonly b:number;
readonly c:number;//编译错误
constructor(){
this.b=0;
}
}
成员函数
成员函数也称作方法,声明成员函数与在对象字面量中声明方法类似的
class Circle{
reduis:number = 1;
area():number{
return Math.PI * this.radius * this.radius;
}
}
成员存取器
成员存取器由get和set方法构成,并且会在类中声明一个属性,成员存取器的定义方式与对象字面量中的属性存取器的定义方式是完全相同的。
如果一个类属性同时定义了get方法和set方法,那么get方法的返回值类型必须与set方法的参数类型一致,否则编译错误
class C{
private _foo:number = 0;
get foo():number{
return this._foo;
}
set foo(value:number){
this._foo = value;
}
// 编译错误 get 和 set 存取器必须具有相同的类型
private _bar:string = '';
get bar():string{
return this._bar;
}
set bar(value:number){
this._bar = value;
}
}
索引成员
类的索引成员会在类的类型中引入索引签名。索引签名包含两种,分别为字符串索引签名和数值索引签名,实际应用中,定义类的索引成员并不常见。类中所有的属性和方法必须符合字符串索引签名定义的类型。同时,只有当类具有类似数组的行为时,数值索引签名才有意义。
class A{
x:number = 0
[prop:string]:number;
[prop:number]:number;
}
// 类的索引成员上不允许定义可访问性修饰符
成员可访问性
成员可访问性定义了类的成员允许在何处被访问。ts为类成员提供了三种可访问性修饰符:
- public
- protected
- private
这三种可访问性修饰符是ts对js的补充,在js中不支持
public
默认情况下,类的所有成员都是公有,可以省略public修饰符
类的成员没有访问限制,可在类的内部、外部以及派生类的内部访问
protected
类的受保护成员允许在当前类的内部和派生类的内部方位,不允许在当前类的外部访问
private
类的私有成员只允许在当前类的内部被访问
私有字段
2020年1月,ECMAScript标准引入了一个新特性,那就是允许在类中定义私有字段,意味着js将原生的支持类的私有成员。ts也从3.8版本开始支持该特性
ECMAScript 类的私有字段 在字段标识符前添加一个“#”符号,不论定义私有字段还是访问私有字段,都需要在私有字段名签前添加一个“#”符号,未来ts是否会弃用private修饰符还在讨论中...
class Circle{
#radius:number;
constructor(){
this.#radius = 1;
}
}
const circle = new Circle();
circle.#radius; //不允许访问
构造函数
构造函数用于创建和初始化类的实例。与普通函数相同,在构造函数中也可以定义可选参数、默认值参数和剩余参数。但是构造函数不允许定义返回值类型,因为构造函数的返回值类型永远为类类型。也可以使用访问性修饰符和重载
class Circle{
radius:number;
constructor(r:number){
this.radius = r;
}
}
class A{
constructor(a:number=0,b?:boolean,...c:string[]){}
}
class A{
constructor(x:number,y:number);
constructor(s:sring);
constructor(xs:number|string,y?:number){}
}
参数成员
ts提供了一种简介语法能够把构造函数的形式参数声明为类的成员变量,叫做参数成员。只要为构造函数参数列表中的形式参数添加任何一个可访问性修饰符或者readonly修饰符,该形式参数就成了参数成员,进而会被声明为类的成员变量
class A{
constructor(private x:number)
}
继承
继承是面向对象程序设计的三个基本特性之一,ts中的类也支持继承,定义类时可以使用extends关键字来指定要继承的类
当派生类继承了基类后,就自动继承了基类的非私有成员
class Shape{
color:string = 'black';
switchColor(){
this.color = this.color === 'black'? 'white':'black'
}
}
class Circle extends Shape{};
const circle = new Circle();
circle.color; // 'black'
circle.swtichColor();
circle.color; // 'white'
重写基类成员
在派生类中可以重写基类的成员变量和成员函数。在重写成员变量和成员函数时,需要派生类中定义与基类中同名的成员变量和成员函数
在派生类中可以通过super关键字来访问基类中的非私有成员
若派生类重写了基类中的受保护成员,则可以将该成员的可访问性设置为受保护的或公有的,也就是派生类中只允许放宽基类成员的可访问性
由于派生类是基类的子类型,因此在重写基类的成员时需要保证子类型兼容性
class Shape(){
color:string = 'black';
swtichColor(){
this.color = this.color === 'black'?'green':'red';
}
}
class Circle extends Shape{
color:string = 'red';
switchColor(){
this.color = this.color === 'red'?'green':'red'
}
}
const circle = new Circle();
circle.color; // 'red'
circle.switchColor();
circle.color; // 'green'
派生类实例化
在派生类的构造函数中必须调用基类的构造函数,否则将不能正确的实例化派生类,
在派生类的构造函数中使用“super()"调用基类的构造函数,super()必须放在派生类构造函数第一行
class Circle extends Shape{
redius:number;
constructor(){
super();
this.radius= 1;
}
}
实例化派生类时的初始化顺序如下:
- 初始化基类的属性
- 调用基类的构造函数
- 初始化派生类的属性
- 调用派生类的构造函数
单继承
ts中的类仅支持单继承,不支持多继承
接口继承类
ts允许接口继承类,若接口继承了一个类,那么该接口会继承基类中的所有成员的类型,接口继承类时,接口不但会继承基类的共有成员类型,还会继承基类的受保护成员类型和私有成员类型
如果接口从基类继承了非公有成员,那么该接口只能由基类或基类的子类来实现
class A{
x:string = '';
y():boolean{
return true;
}
}
interface B extends A{}
declare const b:B;
b.x //类型为string
b.y()//类型为boolean
实现接口
虽然一个类只允许继承一个基类,但是可以实现一个或多个接口,实现多个接口时,接口名之间使用","分隔
如果类的定义中声明了要实现的接口,那么这个类就需要实现接口中定义的类型成员
interface Color{
color:string;
}
interface Shape{
area():number;
}
class Circle implements Shape,Color{
radius:number = 1;
clolor:string = 'black';
area():number {
return Math.PI * this.radius * this.radius;
}
}
静态成员
类的定义中可以包含静态成员,类的静态成员不属于类的某个实例,而是属于类的本省,类的静态成员使用static关键字定义,并且只允许通过类名来访问
class Circle{
static version:string = '1.0';
}
const version = Circle.version; // '1.0'
静态成员可访问性
类的静态成员也可以定义不同的可访问性,如public、private和protected
类的public静态成员对访问没有限制,可以在当前类的内部、外部以及派生类的内部访问。
类的protected静态成员允许在当前类的内部和派生类的内部访问,但是不允许在当前类的外部访问
类的private静态成员只允许在当前类的内部访问。
继承静态成员
类的public静态成员和protected静态成员也可以被继承
抽象类和抽象成员
ts也支持定义抽象类和抽象类成员,抽象类和抽象类成员都使用abstract关键字定义
抽象类
定义抽象类时,只需要在class关键字之前添加abstract关键字即可
抽象类与具体类的一个重要区别是,抽象类不能被实例化,也就是不允许new运算符来创建一个抽象类的实例
抽象类的作用是作为基类使用,派生类可以继承抽象类,抽象类也可以继承其他抽象类
抽象类中允许(通常)包含抽象成员,也允许包含非抽象成员
abstract class A{}
abstract class Base{}
class Derived extends Base{};
abstract class Derived extends Base{}
抽象成员
在抽象类中允许声明抽象成员,抽象成员不允许包含具体实现代码
abstract class A {
abstract a:string;
abstract b:number;
abstract method():string;
abstract get accessor():string;
abstract set accessor(value:string);
}
如果一个具体类继承了抽象类,那么具体的派生类中必须实现抽象类基类中的所有抽象成员,因此抽象类的抽象成员不能声明为private,否则将无法再派生类中实现该成员,若没有正确的在具体的派生类中实现抽象成员,将产生编译错误
abstract class A {
abstract a:string;
abstract method():string;
abstract get accessor():string;
abstract set accessor(value:string);
}
class Derived extends Base{
//实现抽象属性a
a:string = '';
private _accessor:string = '';
get accessor():string{
return this._accessor;
}
set accessor(value:string){
this._accessor = value;
}
//实现抽象方法method
method():boolean{
return true;
}
}
this类型
在 TypeScript 中,this
关键字在类中被用来引用当前实例。这是一种特殊的类型,它可以在方法链式调用中很有用。你可以在一个方法中返回 this
,从而允许链式调用其他方法。在 TypeScript 中,你可以显式声明一个方法返回 this
类型。
这是一个使用 this
类型的简单例子:
class Counter {
private value: number = 0;
increment() {
this.value++;
return this; // 这里返回 `this` 类型
}
getValue() {
return this.value;
}
}
let counter = new Counter();
counter.increment().increment().increment(); // 链式调用
console.log(counter.getValue()); // 输出 3
在这个例子中,increment
方法返回了 this
类型,这样你就可以连续调用 increment
方法。
类类型
在 TypeScript 中,类不仅仅是创建对象的蓝图,它们也可以被当作类型使用。你可以声明一个特定类类型的变量,然后这个变量只能被赋予这个类的实例。这有助于在编译时期进行类型检查,以确保你的代码更加健壮。
下面是一个例子:
class Point {
x: number;
y: number;
}
let p: Point; // 这里,我们声明了一个变量 p,它的类型是 Point 类型。
p = { x: 10, y: 20 }; // 错误:赋值的对象不是 Point 类型
p = new Point(); // 正确:赋值的是 Point 的实例
在这个例子中,我们声明了一个类 Point
,然后我们声明了一个变量 p
,它的类型是 Point
。当我们试图将一个字面量对象赋值给 p
时,TypeScript 报错了,因为这个对象并不是 Point
类的实例。然后,我们新建一个 Point
的实例并赋值给 p
,这样就没有问题了。
类类型也可以用在函数参数中,以确保传入的参数是特定类的实例:
class Point {
x: number;
y: number;
}
function drawPoint(p: Point) {
// draw something
}
drawPoint(new Point()); // 正确
drawPoint({ x: 10, y: 20 }); // 错误
在这个例子中,函数 drawPoint
的参数必须是 Point
类的实例。如果你试图传入一个非 Point
类的实例,TypeScript 就会报错。