Typescript 基础篇

226 阅读24分钟

TypeScript 由微软开发的自由和开源的编程语言。TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。

一、JavaScript 与 TypeScript 的区别

TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,支持 ECMAScript 6 标准(ES6 教程), 因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。

TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。

image.png

二、TypeScript基础语法

1、TypeScript保留的关键字

breakascatchswitch
caseifthrowelse
varnumberstringget
moduletypeinstanceoftypeof
publicprivateenumexport
finallyforwhilevoid
nullsuperthisnew
inreturntruefalse
anyextendsstaticlet
packageimplementsinterfacefunction
dotryyieldconst
continue

2、空白和换行

TypeScript 会忽略程序中出现的空格、制表符和换行符。

空格、制表符通常用来缩进代码,使代码易于阅读和理解。

3、TypeScript 区分大小写

TypeScript 区分大写和小写字符。

4、分号是可选的

每行指令都是一段语句,你可以使用分号或不使用, 分号在 TypeScript 中是可选的,建议使用。

以下代码都是合法的:

console.log("Runoob")
console.log("Google");

如果语句写在同一行则一定需要使用分号来分隔,否则会报错,如:

console.log("Runoob");console.log("Google");

5、TypeScript 支持两种类型的注释

  • 单行注释 ( // )  在 // 后面的文字都是注释内容。
  • 多行注释 (/* */) 这种注释可以跨越多行。

注释实例:

// 这是一个单行注释
 
/* 
 这是一个多行注释 
 这是一个多行注释 
 这是一个多行注释 
*/

6、TypeScript 与面向对象

面向对象是一种对现实世界理解和抽象的方法。

TypeScript 是一种面向对象的编程语言。

面向对象主要有两个概念:对象和类。

  • 对象:对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
  • :类是一个模板,它描述一类对象的行为和状态。
  • 方法:方法是类的操作的实现步骤。

下图中 girl、boy 为类,而具体的每个人为该类的对象

image.png

TypeScript 面向对象编程实例:

class Site { 
    name():void { 
        console.log("Runoob") 
     } 
} 
var obj = new Site(); 
obj.name();

以上实例定义了一个类 Site,该类有一个方法 name(),该方法在终端上输出字符串 Runoob。

new 关键字创建类的对象,该对象调用方法 name()。

编译后生成的 JavaScript 代码如下:

var Site = /** @class */ (function () { 
    function Site() {
    } 
    Site.prototype.name = function () { 
        console.log("Runoob"); 
    }; 
    return Site; 
}()); 
var obj = new Site(); 
obj.name();

执行以上 JavaScript 代码,输出结果如下:

Runoob

三、TypeScript基础类型

1. 类型介绍

JavaScriptTypeScript数据类型关键字描述
Numbernumber数字类型number双精度 64 位浮点值。它可以用来表示整数和分数
Stringstring字符串类型string一个字符系列,使用单引号(')或双引号(")来表示字符串类型。反引号(`)来定义多行文本和内嵌表达式。
Booleanboolean布尔类型boolean表示逻辑值:true 和 false。
Undefinedundefinedundefinedundefined用于初始化变量为一个未定义的值
Nullnullnullnull表示对象值缺失。
voidvoidvoid用于标识方法返回值的类型,表示该方法没有返回值 function hello(): void { alert("Hello Runoob"); }
Symbolsymbol
BigIntbigint

原始类型都遵循冒号加类型的写法 :Type

symbol 与其子类型 unique symbol,symbol类型不同于其他原始类型,它不存在字面量形式,即不能作为属性修饰符使用,为了能够将一个Symbol值视作表示固定值的字面量,TypeScript引入了“unique symbol”类型。

// 因为是字面量形式,必须使用const声明
const x: unique symbol = Symbol();

const y: symbol = Symbol();

interface Foo {
  [x]: string; // 正确

  [y]: string;
	//   ~~~~~~
	//  错误:接口中的计算属性名称必须引用类型为字面量类型
	//  或'unique symbol'的表达式
}

Nullable 类型:

单一值:undefined 和 null

undefined:

在 JavaScript 中, undefined 是一个没有设置值的变量。

typeof 一个没有值的变量会返回 undefined。

undefined 类型只包含一个可能值,即 undefined 值

null:

null 类型只包含一个可能值,即 null 值。

在 JavaScript 中 null 表示 "什么都没有"。

null是一个只有一个值的特殊类型。表示一个空对象引用。

用 typeof 检测 null 返回是 object。

赋值情况:

undefined 值和 null 值允许赋值给顶端类型,同时 undefined 值也允许赋值给void类型。

即 Null 和 Undefined 是其他任何类型(包括 void)的子类型,可以赋值给其它类型,如数字类型,此时,赋值后的类型会变成 null 或 undefined。

而在TypeScript中启用严格的空校验(--strictNullChecks)特性,就可以使得null 和 undefined 只能被赋值给 void 或本身对应的类型,示例代码如下:

// 启用 --strictNullChecks
let x: number;
x = 1;            // 编译正确
x = undefined;    // 编译错误
x = null;         // 编译错误

上面的例子中变量 x 只能是数字类型。如果一个类型可能出现 null 或 undefined, 可以用 | 来支持多种类型,示例代码如下:

// 启用 --strictNullChecks
let x: number | null | undefined;
x = 1; // 编译正确
x = undefined;    // 编译正确
x = null;    // 编译正确

2. 基础类型

布尔类型(boolean)

TypeScript 中的布尔类型用于表示逻辑值,只有两个可能的取值: truefalse。在 TypeScript 中,可以使用关键字 boolean 来定义布尔类型变量。以下是一个示例:

let isFinished: boolean = false;
let hasCompleted: boolean = true;

console.log(isFinished); // 输出:false
console.log(hasCompleted); // 输出:true

可以通过赋值操作符将布尔值赋给变量,并可以根据布尔值进行逻辑判断或条件控制流程。

字符串类型(string)

字符串类型(string)是在 TypeScript 中表示文本的数据类型。它用于存储包含字符序列的值,可以是字母、数字、符号或其他字符的组合。

在 TypeScript 中,可以使用双引号(")单引号(')将字符串字面量括起来,例如:

let message: string = "Hello, TypeScript!"; // 使用双引号
let name: string = 'Alice'; // 使用单引号

你还可以使用模板字符串(template strings)来表示多行文本和包含变量的文本:

let multiLineText: string = `这是
多行文本`;

let age: number = 25;
let greeting: string = `我叫 ${name},今年 ${age} 岁。`;

注意,字符串是不可变的,也就是说一旦创建就不能修改。如果要对字符串进行修改操作,可以使用字符串方法和操作符。

在 TypeScript 中,我们还可以使用一些内置函数和方法来处理字符串,例如 length 属性获取字符串的长度、concat 方法拼接字符串、toUpperCase 方法将字符串转换为大写等。你可以根据实际需求选择合适的方法来操作字符串。

数字类型(number)

在 TypeScript 中,数字类型用于表示数值,可以表示整数浮点数。可以使用关键字 number 来定义数字类型变量。以下是一个示例:

let age: number = 30;
let height: number = 1.75;

console.log(age); // 输出:30
console.log(height); // 输出:1.75

数字类型的变量可以进行常见的数学运算,比如加减乘除等。此外,TypeScript 还支持其他类型的数字,如二进制、八进制和十六进制。例如:

let binary: number = 0b1010; // 二进制
let octal: number = 0o744; // 八进制
let hex: number = 0xabc; // 十六进制

console.log(binary); // 输出:10
console.log(octal); // 输出:484
console.log(hex); // 输出:2748

需要注意的是,数字类型在 TypeScript 中是一种原始类型,不能和字符串类型进行直接的运算操作,需要进行类型转换。例如:

let num: number = 10;
let str: string = "20";

let result = num + Number(str);
console.log(result); // 输出:30

在进行类型转换时,可以使用 Number()、parseInt() 等方法将字符串类型转换成数字类型。

  1. string 字面量类型是 string 类型 的子类型
const a: string = 'hello';
//    ~ Type 'string' is not assignable to type '"world"'.
const b: 'world' = a;
  1. 数字字面量——

number字面量类型

bigint字面量类型

const a: number = 1;

const b: 2 = 1; // 或者赋值非 1,都是不允许的
//    ~ Type '1' is not assignable to type '2'.

3. 枚举类型(Enum)

枚举类型(Enum)是 TypeScript 提供的一种数据类型,用于定义一组命名的常量值。通过枚举类型,可以为一组相关的常量赋予有意义的名称,提高代码的可读性和可维护性。

枚举类中,将这些属性都默认定义成了数字,默认是从0开始

  1. 同时定义数值型枚举成员和字符串枚举成员
  2. 必须为紧跟在字符串枚举成员之后的数值型枚举成员指定一个初始值
  3. 可以使用表达式

数字枚举

数字枚举 : 枚举类型中的每一个常量都是数字

在 TS 中, 枚举内的每一个常量, 当你不设置值的时候, 默认就是 number 类型

enum Pages {
    ONE,    // 0
    TWO,    // 1
    THREE   // 2
}

你在枚举内的常量, 第一个默认值是 0, 后面的依次 +1 递增

此时

Pages.ONE => 0

Pages.TWO => 1

Pages.THREE => 2

我们也可以自己指定值

enum Pages {
    ONE = 10,    // 10
    TWO = 20,    // 20
    THREE = 30   // 30
}

这个时候枚举集合内的常量就是我们指定好的值,我们也可以指定部分值

enum Pages {
    ONE = 10,    // 10
    TWO,         // 11
    THREE        // 12
}

指定常量后面的未指定常量, 就会按照 +1 的规则一次递增

enum Pages {
    ONE,         // 0
    TWO = 10,    // 10
    THREE        // 11
}
enum Pages {
    ONE,         // 0
    TWO = 10,    // 10
    THREE,       // 11
    FOUR = 30,   // 30
    FIVE         // 31
}

字符串枚举

字符串枚举 : 枚举集合中的每一个常量的值都是 string 类型。

在 TS 内, 你必须要指定一个值, 才可能会出现 string 类型。

enum Direction {
  UP = 'up',
  RIGHT = 'right',
  DOWN = 'down',
  LEFT = 'left'
}

在 TS 中, 枚举常量和任何内容都是不一样的, 包括原始字符串。

function util(dir: Direction) {}

image.png

这是因为, 在 TS 中, 枚举内的每一个常量都是一个独一无二的值。所以当你用枚举去限定一个数据的时候, 用的时候也只能用枚举内的值。这样也避免你因为手误出现的单词错误, 比如你会不会认为 'form' 和 'from' 是一个单词呢。

异构枚举

异构枚举 : 其实就是在一个枚举集合内同时混合了数字枚举字符串枚举

但是你大概率是不会这样使用的, 因为我们作为一组数据的集合, 一般不会把数字和字符串混合在一起使用。

enum Info {
  ONE,
  UP = 'up',
  TWO = 2,
  LEFT = 'left'
}

在这里有一个点需要注意。因为在枚举集合内, 当某一个 key 你没有设置值的时候, 会默认按照上一个的值 +1

所以如果前一个是 字符串枚举, 那么下一个必须要手动赋值, 不然会报错;如果前一个是 数字枚举, 那么下一个可以不必要手动赋值, 会按照上一个 +1 计算。

枚举合并

在 TS 内的枚举, 是支持合并的。多个枚举类型可以分开书写, 会在编译的时候自动合并

enum Direction {
  UP = 'up',
  RIGHT = 'right',
  DOWN = 'down',
  LEFT = 'left'
}

enum Direction {
  TOP = 'top',
  BOTTOM = 'bottom'
}

function util(dir: Direction) {}

util(Direction.BOTTOM)
util(Direction.LEFT)

这里定义的两个枚举都叫做 Direction, 会在编译的时候自动放在一起, 不会出现冲突

反向映射

TS 内的数字枚举, 在编译的时候, 会同时将 key 和 value 分别颠倒编译一次。

enum Pages {
    ONE,    // 0
    TWO,    // 1
    THREE   // 2
}

以这个为例, 他是如何进行编译的呢?

var Pages;
(function (Pages) {
    Pages[Enum["ONE"] = 0] = "ONE"
    Pages[Enum["TWO"] = 1] = "TWO"
    Pages[Enum["THREE"] = 2] = "THREE"
})(Pages || (Pages = {}));

编译完毕的结果

Pages = {
    ONE: 0,
    TWO: 1,
    THREE: 2,
    '0': 'ONE',
    '1': 'TWO',
    '2': 'THREE'
}

也就是说, 我们在 TS 内使用的时候, 如果是数字枚举,那么我们可以通过 key 得到对应的数字, 也可以通过对应的数字得到对应的 key。

enum Pages {
    ONE,    // 0
    TWO,    // 1
    THREE   // 2
}

console.log(Pages.ONE)    // 0
console.log(Pages.TWO)    // 1
console.log(Pages.THREE)  // 2
console.log(Pages[0])     // 'ONE'
console.log(Pages[1])     // 'TWO'
console.log(Pages[2])     // 'THREE'

常量枚举

常量枚举, 是在枚举的基础上再加上 const 关键字来修饰。会在编译的时候, 把枚举内容删除, 只保留编译结果。并且对于数字枚举来说, 不在支持反向映射能力, 只能利用 key 来访问

非常量枚举

enum Pages {
    ONE,    // 0
    TWO,    // 1
    THREE   // 2
}

console.log(Pages.ONE)
console.log(Pages.TWO)
console.log(Pages.THREE)

编译完毕的 js 文件

常量枚举

const enum Pages {
    ONE,    // 0
    TWO,    // 1
    THREE   // 2
}

console.log(Pages.ONE)
console.log(Pages.TWO)
console.log(Pages.THREE)

编译完毕的 js 文件

enum D {
   Up,
   Down = 2,
   Left = 'LEFT',
   Right,
// ~~~~~
//  编译错误!枚举成员必须有一个初始值

   Black = 0 + 0,
// ~~~~~
// 编译错误!在带有字符串成员的枚举中不允许使用计算值
	 White = Black,
   Blue = `Blue`
}

enum Bar {
		A = -1,          // 一元运算符:“+”“-”“~”
		B = 1 + 2,       // 二元运算符:“+”“-”“*”“**”“/”“%”“<<”“>>”“>>>”“&”“|”“^”
		C = (4 / 2) * 3, // 分组运算符(小括号)
		A = 'A'.length,
		B = Math.pow(2, 3),
}

4. 顶端类型(any\unknown)

any

any表示任意类型,放弃了ts类型检查,ts中应该少用

1、TypeScript所有类型都是any类型的子类型。可以将任何类型的值赋值给any类型;

2、同时,TypeScript也允许将any类型赋值给任何其他类型

let value: any;

value = true;             // OK
value = 42;               // OK
value = "Hello World";    // OK
value = [];               // OK
value = {};               // OK
value = Math.random;      // OK
value = null;             // OK
value = undefined;        // OK
value = new TypeError();  // OK
value = Symbol("type");   // OK

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。

unknown

unknown是暂时未知类型(之后会知道),仍然会进行ts的类型检查

unknown的引入

1、与any类型是一致的,也是任何其他类型都能够赋值给unknown类型

2、比any类型更安全的顶端类型,因为unknown类型只允许赋值给any类型和unknown类型,而不允许赋值给任何其他类型

这是我们之前看到的相同的一组赋值示例,这次使用类型为 unknown 的变量:

复制代码
let value: unknown;

value = true;             // OK
value = 42;               // OK
value = "Hello World";    // OK
value = [];               // OK
value = {};               // OK
value = Math.random;      // OK
value = null;             // OK
value = undefined;        // OK
value = new TypeError();  // OK
value = Symbol("type");   // OK

value 变量的所有赋值都被认为是类型正确的。

而当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

let x: unknown;

// 正确
const a1: any = x;
const b1: unknown = x;

// 错误
const a2: boolean   = x;
const b2: string    = x;
const c2: number    = x;
const d2: bigint    = x;
const e2: symbol    = x;
const f2: undefined = x;
const g2: null      = x;

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观的说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 x 中存储了什么类型的值。

如何使用 unknown:

缩小 unknown 类型范围

我们可以通过不同的方式将 unknown 类型缩小为更具体的类型范围,包括 typeof 运算符,instanceof 运算符和自定义类型保护函数。所有这些缩小类型范围的技术都有助于 TypeScript 的基于控制流的类型分析

编译器会强制我们在使用时将其细化为某种具体类型

function f2(message: unknown) {
  	// 这里使用了 typeof 去判断某种具体类型
    if (typeof message === 'string') {
        return message.length;
    }
}

f2(undefined);

function stringifyForLogging(value: unknown): string {
  if (typeof value === "function") {
    // Within this branch, `value` has type `Function`,
    // so we can access the function's `name` property
    const functionName = value.name || "(anonymous)";
    return `[function ${functionName}]`;
  }

  if (value instanceof Date) {
    // Within this branch, `value` has type `Date`,
    // so we can call the `toISOString` method
    return value.toISOString();
  }

  return String(value);
}

联合类型和交叉类型中的 unknown

因为任何类型都可以赋给 unknown ,相当于 T extends unknown -> true ,反之不然。

联合类型取最大集合,任何类型和 unknown 类型的联合类型都会得到 unknown :

type U1 = unknown | null; // unknown
type U2 = unknown | undefined; // unknown
type U3 = unknown | number; // unknown
type U4 = unknown | boolean; // unknown
type U5 = unknown | string[]; // unknown
type U6 = unknown | any; // any

在交叉类型中,取最小集合,能够赋值给 T 的一定能够赋给 unknown ,但是能够赋给 unknown 的不能赋给 T ,所以任何类型 T 跟 unknown 的交叉类型都会得到 T 。

type U7 = unknown & null; // null;
type U8 = unknown & undefined; // undefined;
type U9 = unknown & number; // number;
type U10 = unknown & boolean; // boolean;
type U11 = unknown & string[]; // string[]
type U12 = unknown & any; // any;

因为 unknown <-> any 时可以互相赋值的,在这两个例子中 unknown & any 和 unknown | any TS 编译器都推断为 any 是为了更好的向后兼容性。

unknown 类型上的运算符

由于 JS 的弱类型特点,我们进行运算时,如果不是数字类型,内部会根据一定的规则转换到数字类型。

对于 unknown 类型,在没有进行类型收窄时,TS 强制我们只能对其执行等比较操作符,而不能执行加减乘除等运算。

你可以在类型为 unknown 的值上使用的运算符只有四个相等和不等运算符:

  • ===
  • ==
  • !==
  • !=
function f10(x: unknown) {
    x == 5;
    x !== 10;
    x >= 0;  // Error
    x + 1;  // Error
    x * 2;  // Error
    -x;  // Error
    +x;  // Error
}

如果要对类型为 unknown 的值使用任何其他运算符,则必须先指定类型(或使用类型断言强制编译器信任你)。

5. 尾端类型(never)

never

从 TypeScript 2.0起引入了 never 类型,它作为 TS 中的Bottom Type用来表示当前不能返回值。它跟 void 类型 的区别在于,void 表示的是返回为空(undefined),实际上是有返回值的,而never表示的是永不返回。

never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值,永远不会发生的类型。这意味着声明为 never 类型的变量只能被 never 类型所赋值

在函数中它通常表现为抛出异常无法执行到终止点(例如无限循环)

let x: never;
let y: number;

// 编译错误,数字类型不能转为 never 类型
x = 123;

// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error('exception')})();

// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error('exception')})();

// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
    throw new Error(message);
    // <- 该函数永远无法执行到末尾,返回值类型为'never'
}

// 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
    while (true) {}
}

一旦有人告诉你,never 表示一个从来不会优雅的返回的函数时,你可能马上就会想到与此类似的 void,然而实际上,void 表示没有任何类型,never 表示永远不存在的值的类型。

当一个函数返回空值时,它的返回值为 void 类型,但是,当一个函数永不返回时(或者总是抛出错误),它的返回值为 never 类型。void 类型可以被赋值(在 strictNullChecking 为 false 时),但是除了 never 本身以外,其他任何类型不能赋值给 never。

Typescript中never类型的分析和使用

6. void类型

void类型表示函数没有返回值,或者说返回类型为空。在TypeScript中,使用’void’关键字来指定函数的返回类型为’void’,可以帮助我们更好地进行类型检查和代码约束。

void类型的使用

当我们声明一个函数时,如果函数没有返回值,我们可以使用void来明确指定其返回类型。例如:

function sayHello(): void {
    console.log("Hello TypeScript");
}

在上面的例子中,函数sayHello没有返回值,因此我们使用了void来定义其返回类型。

  • void类型的变量不能被赋予任何值,只能赋值为undefinednull
let variable: void;
variable = undefined; // 合法
variable = null; // 合法
variable = 123; // 非法,'void'类型不能赋值为其他类型
  • 当一个函数的返回类型被定义为’void’时,我们不能使用该函数的返回值。
function add(a: number, b: number): void {
    const result = a + b;
    return result; // 非法,'void'类型的函数不能返回值
}

void类型的使用场景

  • 对于没有返回值的函数,使用void类型可以提高代码的可读性和可维护性。
function log(message: string): void {
    console.log(message);
}

// 调用log函数,不需要关心返回值
log("Hello TypeScript");
  • 对于需要明确表示函数没有返回值的场景,使用void类型可以提供更好的类型检查和约束。
function printSum(a: number, b: number): void {
    const result = a + b;
    console.log("Sum:", result);
}

// 调用printSum函数,不需要对返回值进行处理
printSum(2, 3);

7. 数组类型(Array)

数组类型(Array)用于表示一个由相同数据类型的值组成的集合。

在 TypeScript 中,可以使用下面的语法来声明数组类型:

方式一:类型 + 方括号[]

TElement[]

let a = number[];
let b = (number | string)[];

方式二: 泛型数组类型表示法:

Array<TElement>

let a: Array<string | number>;

let b: Array<{ x: number; y: number }>;

只读数组:

ReadonlyArray、readonly、Readonly 三种方式来定义一个只读数组

// 使用“ReadonlyArray<T>”内置类型
const red: ReadonlyArray<number> = [255, 0, 0];
// 使用readonly修饰符
const red: readonly number[] = [255, 0, 0];
// 使用“Readonly<T>”工具类型
const red: Readonly<number[]> = [255, 0, 0];

注意:允许将常规数组类型赋值给只读数组类型,但是不允许将只读数组类型赋值给常规数组类型,即只允许收敛赋值

8. 元组类型(Tuple)

元组类型(Tuple)是 TypeScript 中的一种数据类型,它允许你存储一个固定长度的、类型可以不同的数据序列

数组中元素的数据类型都一般是相同的(any[] 类型的数组可以不同),如果存储的元素数据类型不同,则需要使用元祖

元组中允许存储不同类型的元素,元组可以作为参数传递给函数

[T0, T1, ..., Tn]

[T0?, T1?, ..., Tn?]

[...T[]]

const tuple: [number, ...string[]] = [0, 'a', 'b'];
var mytuple = [10,"Runoob"]; // 创建元组 
console.log(mytuple[0]) 
console.log(mytuple[1])

只读元组:

  • 使用readonly修饰符
  • 使用“Readonly”工具类型。

注意:只允许收敛赋值

元组与数组兼容性:

  • 元组是数组的子类型
  • 只读元组是只读数组的子类型
  • 元组还可以赋值给只读数组

元组运算:

我们可以使用以下两个函数向元组添加新元素或者删除元素:

  • push() 向元组添加元素,添加在最后面。
  • pop() 从元组中移除元素(最后一个),并返回移除的元素。
var mytuple = [10,"Hello","World","typeScript"];
console.log("添加前元素个数:"+mytuple.length) // 返回元组的大小 
mytuple.push(12) // 添加到元组中 
console.log("添加后元素个数:"+mytuple.length) 
console.log("删除前元素个数:"+mytuple.length) 
console.log(mytuple.pop()+" 元素从元组中删除") // 删除并返回删除的元素 
console.log("删除后元素个数:"+mytuple.length)
    

添加前元素个数:4
添加后元素个数:5
删除前元素个数:5
12 元素从元组中删除
删除后元素个数:4

9. 对象类型

对象是包含一组键值对的实例。 值可以是标量、函数、数组、对象等,如下实例:

var object_name = {
    key1: "value1", // 标量 
    key2: "value", 
    key3: function() { 
        // 函数 
    }, 
    key4:["content1", "content2"] //集合 
}
  • Object类型(首字母为大写字母O)
  • object类型(首字母为小写字母o)
  • 对象类型字面量

Object 类型

JavaScript中的公共原型对象——Object.prototype

该类型的主要作用是描述JavaScript中几乎所有对象都共享(通过原型继承)的属性和方法:

toString()

toLocaleString()

valueOf()

hasOwnProperty()

isPrototypeOf()

propertyIsEnumerable()

注意:

为了符合 js 中对原始类型的自动封箱操作,除了 undefined 和 null ,一些原始类型也能赋值给它。

代表原型对象,也被TS 官方建议不应该使用。

object 类型

object类型表示仅是一个对象类型。Object 的升级版,也可近似的理解为一个空对象类型 {} ,所以关注点不是该对象类型具体包含了哪些属性,只有共享属性和方法。

对象类型字面量

自定义属性对象。

{
   TypeMember;
   TypeMember;
   TypeMember;
   ...
}

TypeMember——

  • 属性签名
  • 调用签名
  • 构造签名
  • 方法签名
  • 索引签名
  1. 属性签名

{

PropertyName: Type;

}

type obj = {
  x: number;

  // y: string | number | unique symbol
 [y]: boolean;

  // z: any;
  z;
}

赋值兼容问题:

let point: { x: number; y?: number;};

// 正确
point = { x: 0, y: 0};

point = { x: 0, y: 0, z: 0}; // 编译错误
//                    ~~~~
//                    z是多余属性

const point: {
    x: number;
    y: number;
    [prop: string]: any; // 加索引签名
} = { x: 0, y: 0, z: 0 };

10. 函数类型

TypeScript函数 —— 菜鸟中文网

undefined 可以赋值给 void

function f0(): void {
    return undefined; // 允许
// -
    return null; // 编译错误
}

普通函数类型

字面量方式:

(ParameterList) => Type

let f: () => void;
let add: (x: number, y: number) => number;
let f1: ({ x, y }: { x: number; y: number }) => void;
let f2: (...args: number[]) => void;

调用签名方式:

{

(ParameterList): Type

}

let f: {(): void};
let add: {(x: number, y: number): number};
let f1: {({ x, y }: { x: number; y: number }): void};
let f2: {(...args: number[]): void};

什么时候用签名方式:

// 给函数添加自定义属性
let foo: { (x: number): void; version: string } = f;

总结:字面量方式更简洁,但签名方式有更强的类型表达能力。


函数重载

函数重载:方法名字相同,而参数不同,返回类型可以相同也可以不同。或者可以理解为同一个函数名可以对应着多个函数的实现

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

声明不带有函数体 function add(x: number, y: number): number;

当函数存在多个参数,且针对参数存在不同类型有不用处理逻辑的时候:

function add(x: number, y: number): number; // 重载函数1
function add(x: number[], y: number[]): number[]; // 重载函数2
function add(x: number | number[], y: number | number[]): any { // 函数实现
    if (typeof x === 'number' && typeof y === 'number') {
         return x + y;
    }
    if (Array.isArray(x) && Array.isArray(y)) {
         return [...x, ...y];
    }
}

const a: number = add(1, 2);
const b: number[] = add([1], [2]);
const c: number[] = add(1, [2]); // 编译出错

// 或者用调用签名形式定义
let add: {
  (x: number, y: number): number;
  (x: number[], y: number[]): number[];
}
add = (x: number | number[], y: number | number[]): any => {···}

函数实现类型必须兼容所有重载函数类型:参数兼容,返回值兼容。

从上到下的匹配顺序,把最精确的匹配放到前面

参数类型不同:

function disp(string):void; 
function disp(number):void;

参数数量不同:

function disp(n1:number):void; 
function disp(x:number,y:number):void;

参数类型顺序不同:

function disp(n1:number,s1:string):void; 
function disp(s:string,n:number):void;

如果参数类型不同,则参数类型应设置为 any

参数数量不同你可以将不同的参数设置为可选。

function disp(s1:string):void; 
function disp(n1:number,s1:string):void; 
 
function disp(x:any,y?:any):void { 
    console.log(x); 
    console.log(y); 
} 
disp("abc") 
disp(1,"xyz");

11.接口声明

interface InterfaceName {

PropertyName: Type; // 属性签名

(ParameterList): Type; // 调用签名

PropertyName(ParameterList): Type; // 方法签名

}

interface InterfaceName { 
  // PropertyName: Type; // 属性签名
  x?: number;
  [y: number]?: string;
  
  // (ParameterList): Type; // 调用签名
  (): void;
  
  // PropertyName(ParameterList): Type; // 方法签名
  f(x: boolean): string;
	f: (x: boolean) => string;
	f: {(x: boolean): string};
}

// 函数重载(属于调用签名)
interface add {
  (x: number, y: number): number;
  (x: number[], y: number[]): number[];
}
let add: add = (x: number | number[], y: number | number[]): any => {···}

四、泛型

1. 泛型概念

泛型程序设计是一种编程风格或编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数。允许在定义函数、类、接口等时使用占位符来表示类型,而不是具体的类型

简单理解:先声明占坑,后实例化补位。

使用泛型的主要目的是为了处理不特定类型的数据,使得代码可以适用于多种数据类型而不失去类型检查

泛型的优势

泛型的优势包括:

  • 代码重用:  可以编写与特定类型无关的通用代码,提高代码的复用性。
  • 类型安全:  在编译时进行类型检查,避免在运行时出现类型错误。
  • 抽象性:  允许编写更抽象和通用的代码,适应不同的数据类型和数据结构。

泛型标识符

在泛型中,通常使用一些约定俗成的标识符,比如常见的 T(表示 Type)、UV 等,但实际上你可以使用任何标识符。

T: 代表 "Type",是最常见的泛型类型参数名。

function identity<T>(arg: T): T {
    return arg;
}

K, V: 用于表示键(Key)和值(Value)的泛型类型参数。

interface KeyValuePair<K, V> {
    key: K;
    value: V;
}

E: 用于表示数组元素的泛型类型参数。

function printArray<E>(arr: E[]): void {
    arr.forEach(item => console.log(item));
}

R: 用于表示函数返回值的泛型类型参数。

function getResult<R>(value: R): R {
    return value;
}

U, V: 通常用于表示第二、第三个泛型类型参数。

function combine<U, V>(first: U, second: V): string {
    return `${first} ${second}`;
}

这些标识符是约定俗成的,实际上你可以选择任何符合标识符规范的名称。关键是使得代码易读和易于理解,所以建议在泛型类型参数上使用描述性的名称,以便于理解其用途。

泛型函数

使用泛型来创建一个可以处理不同类型的函数,描述不同参数之间以及参数和函数返回值之间的关系。

普通函数定义泛型

泛型参数列表紧随函数名之后

<T>(x: T): T

const fn1 = function fun1<T> (a:T) { return a }
function identity<T>(arg: T): T {  
    return arg;  
}  
  
// 使用泛型函数  
let result = identity<string>("Hello");  
console.log(result); // 输出: Hello  
  
let numberResult = identity<number>(42);  
console.log(numberResult); // 输出: 42

解析:  以上例子中,identity 是一个泛型函数,使用 <T> 表示泛型类型。它接受一个参数 arg 和返回值都是泛型类型 T。在使用时,可以通过尖括号 <> 明确指定泛型类型。第一个调用指定了 string 类型,第二个调用指定了 number 类型。

function mergeArrays<T, U>(arr1: T[], arr2: U[]): (T | U)[] {
  return arr1.concat(arr2);
}

const numbers = [1, 2, 3];
const strings = ['hello', 'world'];
const result = mergeArrays(numbers, strings);
console.log(result); // [1, 2, 3, 'hello', 'world']
function f3<T, U>(a: T[], f: (x: T) => U): U[] {
    return a.map(f);
}

const a: boolean[] = f3<number, boolean>([0, 1, 2], n => !!n);

箭头函数定义泛型

1.在<>中的泛型后边加上一个“(,)

2.使用类型定义的方式

3.使用extends关键字改写

const foo1 = <T, >(value: T): T => value
   
const foo2: <T>(x: T) => T = x => x
   
const foo3 = <T extends object>(x: T): T => x  

泛型接口

以使用泛型来定义接口,使接口的成员能够使用任意类型:

// 基本语法  
interface Pair<T, U> {  
    first: T;  
    second: U;  
}  
  
// 使用泛型接口  
let pairPair<string, number> = { first"hello"second42 };  
console.log(pair); // 输出: { first: 'hello', second: 42 }

泛型参数列表紧随接口名之后

interface MyArray<T> extends Array<T> {
    first: T | undefined;
    last: T | undefined;
}

一个树形结构:递归使用

type Tree<T> = {
    value: T;
    left: Tree<T> | null;
    right: Tree<T> | null;
};

const tree: Tree<number> = {
    value: 0,
    left: {
        value: 1,
        left: {
            value: 3,
            left: null,
            right: null
        },
        right: {
            value: 4,
            left: null,
            right: null
        }
    },
    right: {
        value: 2,
        left: null,
        right: null
    }
};

泛型类

泛型也可以应用于类的实例变量和方法:

// 基本语法  
class Box<T> {  
    private value: T;  
  
    constructor(value: T) {  
        this.value = value;  
    }  
  
    getValue(): T {  
        return this.value;  
    }  
}  
  
// 使用泛型类  
let stringBox = new Box<string>("TypeScript");  
console.log(stringBox.getValue()); // 输出: TypeScript

解析:  在这个例子中,Box 是一个泛型类,使用 <T> 表示泛型类型。构造函数和方法都可以使用泛型类型 T。通过实例化 Box<string>,我们创建了一个存储字符串的 Box 实例,并通过 getValue 方法获取了存储的值。

泛型约束(Generic Constraints)

有时候你想限制泛型的类型范围,可以使用泛型约束:

实例

// 基本语法  
interface Lengthwise {  
    length: number;  
}  
  
function logLength<T extends Lengthwise>(arg: T): void {  
    console.log(arg.length);  
}  
  
// 正确的使用  
logLength("hello"); // 输出: 5  
  
// 错误的使用,因为数字没有 length 属性  
logLength(42); // 错误  
    

解析:  在这个例子中,定义了一个泛型函数 logLength,它接受一个类型为 T 的参数,但有一个约束条件,即 T 必须实现 Lengthwise 接口,该接口要求有 length 属性。因此,可以正确调用 logLength("hello"),但不能调用 logLength(42),因为数字没有 length 属性。

泛型与默认值

可以给泛型设置默认值,使得在不指定类型参数时能够使用默认类型:

实例
    
// 基本语法  
function defaultValue<T = string>(arg: T): T {  
    return arg;  
}  
  
// 使用带默认值的泛型函数  
let result1 = defaultValue("hello"); // 推断为 string 类型  
let result2 = defaultValue(42);      // 推断为 number 类型  

说明:  这个例子展示了带有默认值的泛型函数。函数 defaultValue 接受一个泛型参数 T,并给它设置了默认类型为 string。在使用时,如果没有显式指定类型,会使用默认类型。在例子中,第一个调用中 result1 推断为 string 类型,第二个调用中 result2 推断为 number 类型。

2. 联合类型(Union Types)

联合类型(Union Types)是 TypeScript 中的一种高级类型,它允许一个变量可以存储多种类型的值。具体来说,联合类型用 | 符号将多个类型进行组合。例如,string | number 表示该变量可以存储字符串数字类型的值。

  1. 字面量类型联合
type T = boolean | string[] | { x: number } | (() => void);
  1. 属性签名联合
interface Circle {
    area: number;
    radius: number;
}

interface Rectangle {
    area: string;
    width: number;
    height: number;
}

type Shape = Circle | Rectangle;

declare const s: Shape;
s.area;
s.radius; // 错误
s.width;  // 错误
s.height; // 错误
  1. 联合类型“U = U0 | U1”中,若U1是U0的子类型,那么联合类型可以化简为“U = U0”

理解记忆:' | ' 表示一种 或 的关系,也即使用任何一个成员都可以满足要求,所以就会得到所有成员共同具有的特性。

注意:

const f1 = (a: number | string) => {
  a.xxx?
  // 这里能使用出 a 的那些方法?
  // 即不能把 a 当作 number
  // 也不能把 a 当作 string
  // 那么,怎么使用 a 变量呢?
}

需要注意的是,当我们在使用联合类型的值时,需要进行类型检查和类型保护。这可以通过类型断言类型保护函数typeofinstanceof 运算符来实现。这种行为也叫做

当涉及到联合类型时,我们必须注意如何进行类型保护,以确保安全地操作变量。以下是几个使用联合类型的代码案例,并展示了不同的类型保护方法:

案例1: 类型断言

function printValue(value: string | number): void {
  if (typeof value === "string") {
    // 使用类型断言,将 value 视为字符串类型
    console.log(value.toUpperCase());
  } else {
    // 使用类型断言,将 value 视为数字类型
    console.log(value.toFixed(2));
  }
}

printValue("hello"); // 输出:HELLO
printValue(3.14159); // 输出:3.14

案例2: 类型保护函数

// 定义类型保护函数,判断是否为字符串
function isString(value: string | number): value is string {
  return typeof value === "string";
}

function printValue(value: string | number): void {
  if (isString(value)) {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

printValue("hello"); // 输出:HELLO
printValue(3.14159); // 输出:3.14

案例3: typeof 和 instanceof 运算符

function printValue(value: string | number): void {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if (value instanceof Number) {
    console.log(value.toFixed(2));
  }
}

printValue("hello"); // 输出:HELLO
printValue(new Number(3.14159)); // 输出:3.14

案例4: switch 语句

function printValue(value: string | number): void {
  switch (typeof value) {
    case "string":
      console.log(value.toUpperCase());
      break;
    case "number":
      console.log(value.toFixed(2));
      break;
  }
}

printValue("hello"); // 输出:HELLO
printValue(3.14159); // 输出:3.14

在这些案例中,我们通过不同的方法进行了类型保护。在每个条件分支中,我们根据变量的类型来执行相应的操作。这样可以确保在类型不一致的情况下,代码能够正确地进行类型处理。需要根据具体的场景选择适合的类型保护方法。

不过我们发现 tpyeof 的有以下的局限性:数组对象、普通对象、日期对象、null 都无法区分

const a = (params: Date | Date[]) => {
  if (typeof params === 'object') {
    params // 此时类型仍然为 Date | Date[]
  }
}

但是我们还能使用 instanceof

instanceof 这个方法主要返回对象的类

const a = (params: Date | Date[]) => {
  if (params instanceof Date) {
    params // 此时类型收窄为 Date
  } else if (params instanceof Array) {
    params // 此时类型收窄为 Array
  }
}

instanceof 的局限性:不支持 TS 独有的类型

type Person = {
  age: number;
}
type Animal = {
  name: string;
}
const a = (params: Person | Animal) => {
  if (params instanceof Person) {  // TODO: 报错,Person 是一个类型,而不是类   
    // ...
  }
}

那么如何解决呢?

in

type Person = {
  age: number;
}
type Animal = {
  name: string;
}
const a = (params: Person | Animal) => {
  if ('name' in params) {  
    params // 这里的类型为 Animal
  } else if ('age' in params) {
    params // 这里的类型为 Person
  }
}

in 的局限性:只能区分部分对象,比如没有相同属性的对象、日期对象、函数

因为我们一直在用 js 的逻辑来判断 ts 里面的逻辑,这是两门语言,根本无法做到一一对应

更多TypeScript缩小,可以阅读文档

类型谓词/类型判断

is

type Person = {
  age: number;
  sex: number;
}
type Animal = {
  name: string;
  legs: string[];
}

const a = (params: Perosn | Animal) => {
  if (isPerson(a)) {
    params // 这里的类型一定为 Person  
  }
}

function isPerson(x: Person | Animal /* 这里可以写 any */): x is Person {
  // 这里可以使用上面所有的类型判断技巧
  return 'age' in x && 'sex' in x // 必须返回 boolean
}
const isAnimal = (x: any): x is Animal => 'name' in x && 'legs' in x && a instanceof Array

有点是支持所有 TS 类型,但是随之而来的缺点就是 麻烦!那么有没有更简单的方法呢?

可辨别联合

type Person = { kind: 'person'; name: string; age: number; }
type Animal = { kind: 'animal'; name: string; }

const a = (params: Person | Animal) => {
  if (params.kind === 'person') {
    params // 这里的类型为 Person 
  } elseif (params.kind === 'animal') {
    params // 这里的类型为 Animal
  }
}
  • 优点:把复杂类型的收窄变成简单类型的对比
  • 局限性:1. 必须加上相同属性 kind; 2. kind 的类型必须是简单类型; 3. 各类型中的 kind 无交集

则称为可辨别联合

断言

type Person = {
  age: number;
}
type Animal = {
  name: string;
}
const a = (params: Person | Animal) => {
  (params as Person).age // 强制告诉 ts 此时类型为 Person
}

思考:any 是否等于所有类型(除 never/unknown/any/void)的联合?为什么?

答案: 不是

const a = (params: number | string) => {
  a // 此时这里只能调用 number 和 string 的共用方法,无法调用比如 split 这种字符串特有方法
}

但是如果是 any,则可以继续使用

const a = (params: any) => {
  a.split() // 不会报错
  a.toFixed() // 不会报错
}

这个时候我们再来看看官方文档

image.png

解释为这是一个特殊的类型,如果你不想让你的类型报错你可以使用这个类型

其实文档也没有说的很清楚,目前来看 TS 绝大部分规则对 any 不生效

const a = (params: any) => {
  const newParams: never = params // TODO: 报错
}

那么什么类型等于所有类型(除 never/unknown/any/void)的联合呢?为什么?

答案: 就是 unknown

**

const a = (params: unknown) => {
  if (typeof === 'number') {
    params.toFixed() // 这里的类型是 number
  } elseif (params instanceof Data) {
    params // 这里的类型是 Date
  }
}

也就是说 unknown 可以收窄为任意类型,那么反过来说就是 unknown 就是所有类型的联合,必须通过收窄后才能使用。

3. 交叉类型

TypeScript 的交叉类型(Intersection Types)是将多个类型合并为一个新的类型的一种方式。通过交叉类型,可以创建一个包含了多个类型特性的类型。

通过使用&符号来表示的,表示一个新类型,该类型包含了多个类型的特性,即所有类型的并集。例如,类型 A & B 表示一个包含类型 A 和类型 B 特性的新类型。

  1. 字面量类型联合
type T1 = boolean & boolean & boolean; // ?
type T2 = boolean & number & string; // ?
  1. 属性签名联合
interface A {
    a: number;
    b: string;
    d: {a: number};
    e: boolean;
    f?: boolean;
}

interface B {
    a: string;
    c: boolean;
    d: {b: number};
    e?: boolean;
    f?: boolean;
}

type C = A & B;

C = {
    a: , // ?
  	b: , // ?
  	c: , // ?
  	d: , // ?
  	e: , // ?
  	f: , // ?
}

理解记忆:' & ' 表示一种 且 (或合)的关系,即所有成员合起来才可以满足要求,所以就会得到所有成员的特性组合。

  1. ' & ' 与 ' | ' 的优先级
// 比函数优先级高
() => bigint | number
() => (bigint | number)

// '&' 的优先级高
A & (B | C) ≡ (A & B) | (A & C)
(A | B) & (C | D) ≡ A & C | A & D | B & C | B & D

4. 索引类型

  1. 属性类型

keyof Type

搜集一个类型的属性类型

interface Point {
    x: number;
    y: number;
  	b(): void;
}

type T = keyof Point; // 'x' | 'y' | 'b'
  1. 属性值类型

T[K]

interface T {
    x: number;
    y: number;
  	b(): void;
}

type T0 = T['x']; // number

5. 映射对象类型

将已有的对象类型映射为新的对象类型

{ [P in K]: T }

type T = { a: string; b: number };

// { a: boolean; b: boolean;  }
type M = { [P in keyof T]: boolean };

/**
 * 将T中的所有属性标记为可选属性
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

type Required<T> = { [P in keyof T]-?: T[P] };

6. 条件类型

  1. extends 关键字:

T extends U ? X : Y

T 是 U 的子类型

// 从联合类型T中删除符合条件的类型
type Exclude<T, U> = T extends U ? never : T;
  1. infer 关键字:

T extends infer U ? U : Y;

type E<T> = T extends infer U ? U : T;
//     ~ 此处只有 T

思考题:实现一个ReturnType,获取某个函数的返回值类型?

type ReturnType<
    T extends (...args: any) => any
> = T extends (...args: any) => infer R ? R : any;

7. 内置工具类型

  • Partial
  • Required
  • Readonly
  • Record<K, T>
  • Pick<T, K>
  • Omit<T, K>
  • Exclude<T, U>
  • Extract<T,U>
  • NonNullable
  • Parameters
  • ConstructorParameters
  • ReturnType
  • InstanceType
  • ThisParameterType
  • OmitThisParameter
  • ThisType
// Partial<T>
interface A {
    x: number;
    y: number;
}

type T = Partial<A>; // { x?: number; y?: number; }

// Required<T>
interface A {
    x?: number;
    y: number;
}
type T0 = Required<A>; // { x: number; y: number; }

// Record<K, T>
type K = 'x' | 'y';
type T = number;
type R = Record<K, T>; // { x: number; y: number; }

// Pick<T, K>, 与之类似的一个 Extract<T, U>
interface A {
    x: number;
    y: number;
}
type T0 = Pick<A, 'x'>;        // { x: number }
type T1 = Pick<A, 'y'>;        // { y: number }
type T2 = Pick<A, 'x' | 'y'>;  // { x: number; y: number }

8. 类型断言

类型断言

expr

const username = document.getElementById('username');

if (username) {
  	username.value // 编译错误!属性'value'不存在于类型'HTMLElement'上

    (<HTMLInputElement>username).value; // 正确
}

as 类型断言

expr as T

(username as HTMLInputElement).value;

强制类型断言

先将expr的类型转换为顶端类型unknown,而后再转换为目标类型

expr as unknown as T

const a = 1 as unknown as boolean; // a: boolean

不可变断言

expr as const

expr

! 类型断言

expr!

从某个类型中剔除undefined类型和null类型

类型守卫

typeof 运算符

// 获取对象类型
const A = {
    a: 1,
    b: true,
    c: () => {}
}
type A = typeof A;

// A
type A = {
    a: number;
    b: boolean;
    c: () => void;
}

———————————————————————————————————————————————————

参考

TypeScript -- 菜鸟教程

TypeScript Deep Dive - 深入理解TypeScript