TypeScript 完全指南:从基础到高级类型系统(一)

TypeScript 语言简介

概述

TypeScript(简称 TS)是微软公司精心打造的一门编程语言,它构建于 JavaScript(简称 JS)的基础之上。其核心目标并非创造一门全新的语言,而是为 JavaScript 注入强大的功能,让它更契合多人协作的企业级项目。可以把 TypeScript 视为 JavaScript 的超集(superset),它完整继承了 JavaScript 的全部语法,这意味着所有 JavaScript 脚本都能当作 TypeScript 脚本使用(不过可能会报错),在此基础上,TypeScript 还增添了一些独有的语法。TypeScript 对 JavaScript 的最大贡献,当属引入了一个独立的类型系统。

类型的概念

类型(type)代表着一组具有相同特征的值。如果两个值具备某种共同特征,那么就可以说它们属于同一种类型。例如,123456 这两个值,它们的共同特点是都能进行数值运算,所以它们都属于“数值”(number)类型。一旦确定了某个值的类型,就表明这个值拥有该类型的所有特征,能够进行该类型允许的所有运算。在编程中,凡是适用于该类型的地方,都可以使用这个值;反之,若使用场景不适合该类型,使用这个值就会触发报错。可以这样理解,类型是人为添加的一种编程约束和用法提示,主要目的是在软件开发过程中,为编译器和开发工具提供更多的验证和帮助,从而提升代码质量,减少错误的发生。

下面是一段简单的 TypeScript 代码,展示了类型系统的作用:

function addOne(n:number) {
  return n + 1;
}

在上述示例中,函数 addOne() 有一个参数 n,其类型为数值(number),这意味着该位置只能传入数值,传入其他类型的值就会报错。

addOne('hello') // 报错

这里,函数 addOne() 传入了字符串 hello,TypeScript 会立即发现类型不匹配,从而报错,提示该位置只能传入数值。而 JavaScript 语言没有这种类型检查功能,在开发阶段很可能无法发现此类问题,代码可能会直接发布,导致用户在使用时遇到错误。相比之下,TypeScript 在开发阶段就会报错,有助于提前发现问题,避免在使用过程中出现错误。此外,函数定义中加入类型信息,还能起到提示作用,让开发者清楚该函数的使用方法。

⚙️动态类型与静态类型

前面提到,TypeScript 的主要功能是为 JavaScript 添加类型系统。大家可能知道,JavaScript 语言本身也有一套自己的类型系统,例如数值 123 和字符串 Hello。然而,JavaScript 的类型系统非常薄弱,且没有使用限制,运算符可以接受各种类型的值。从语法层面来看,JavaScript 属于动态类型语言。

请看下面的 JavaScript 代码:

// 例一
let x = 1;
x = 'hello';

// 例二
let y = { foo: 1 };
delete y.foo;
y.bar = 2;

在例一中,变量 x 声明时值的类型是数值,但后续可以被赋值为字符串。因此,无法提前确定变量的类型,即变量的类型是动态变化的。在例二中,变量 y 是一个对象,它原本有一个属性 foo,但这个属性可以被删除,并且还能新增其他属性。所以,对象的属性情况也是动态的,无法提前预知。

正是由于这些动态变化,JavaScript 的类型系统缺乏强大的约束性,这对于提前发现代码错误极为不利。而 TypeScript 引入了一个更强大、更严格的类型系统,属于静态类型语言。上述代码在 TypeScript 中都会报错:

// 例一
let x = 1;
x = 'hello'; // 报错

// 例二
let y = { foo: 1 };
delete y.foo; // 报错
y.bar = 2; // 报错

在上述示例中,例一报错是因为变量赋值时,TypeScript 已经推断并确定了其类型,后续不允许再赋值为其他类型的值,即变量的类型是静态的。例二报错是因为对象的属性也是静态的,不允许随意增删。TypeScript 的作用就是为 JavaScript 引入这种静态类型特征。

✅静态类型的优点

静态类型具有诸多优点,这也是 TypeScript 所追求的目标:

  1. 有利于代码的静态分析:借助静态类型,无需运行代码,就能确定变量的类型,进而推断代码是否存在错误,这就是代码的静态分析。对于大型项目而言,仅在开发阶段进行静态检查,就能发现许多问题,避免交付有问题的代码,大大降低了线上风险。
  2. 有利于发现错误:由于每个值、每个变量、每个运算符都有严格的类型约束,TypeScript 能够轻松发现拼写错误、语义错误和方法调用错误,为程序员节省大量时间。
let obj = { message: '' };
console.log(obj.messege); // 报错

在这个示例中,不小心将 message 拼写成了 messege,TypeScript 会立即报错,指出该属性未定义。而 JavaScript 遇到这种情况不会报错。

const a = 0;
const b = true;
const result = a + b; // 报错

这是一段合法的 JavaScript 代码,但将数值 a 与布尔值 b 相加是没有意义的。TypeScript 会直接报错,提示运算符 + 不能用于数值和布尔值的相加。

function hello() {
  return 'hello world';
}

hello().find('hello'); // 报错

在这个示例中,hello() 返回的是一个字符串,TypeScript 会发现字符串没有 find() 方法,从而报错。而 JavaScript 只有在运行阶段才会发现这个错误。
3. 更好的 IDE 支持,实现语法提示和自动补全:IDE(集成开发环境,如 VSCode)通常会利用类型信息,提供语法提示功能(编辑器自动提示函数用法、参数等)和自动补全功能(只需键入一部分变量名或函数名,编辑器就能补全后面的部分)。
4. 提供了代码文档:类型信息可以部分替代代码文档,解释代码的使用方法。经验丰富的开发者往往只需查看类型,就能大致推断代码的功能。借助类型信息,很多工具还能直接生成文档。
5. 有助于代码重构:修改他人的 JavaScript 代码往往是一件痛苦的事情,项目规模越大,难度越高,因为不确定修改是否会影响到其他部分的代码。而类型信息大大降低了重构的成本。一般来说,只要函数或对象的参数和返回值保持类型不变,就能基本确保重构后的代码能够正常运行。如果还有配套的单元测试,就可以完全放心地进行重构。在大型的、多人合作的项目中,类型信息的作用尤为显著。

综上所述,TypeScript 有助于提高代码质量,保证代码安全,更适合用于大型的企业级项目,这也是大量 JavaScript 项目转向 TypeScript 的原因。

❌静态类型的缺点

静态类型也存在一些不足之处:

  1. 丧失了动态类型的代码灵活性:动态类型具有极高的灵活性,给予程序员很大的自由,而静态类型则剥夺了这些灵活性。
  2. 增加了编程工作量:引入类型后,程序员不仅要编写功能代码,还需要编写类型声明,确保类型的正确性。这无疑增加了不少工作量,有时会显著延长项目的开发时间。
  3. 更高的学习成本:类型系统通常较为复杂,需要学习的内容较多,这要求开发者付出更高的学习成本。
  4. 引入了独立的编译步骤:原生的 JavaScript 代码可以直接在 JavaScript 引擎中运行,但添加类型系统后,需要增加一个单独的编译步骤,用于检查类型是否正确,并将 TypeScript 代码转换为 JavaScript 代码,才能运行。
  5. 兼容性问题:TypeScript 依赖 JavaScript 生态,需要使用很多外部模块。然而,过去大部分 JavaScript 项目都没有进行 TypeScript 适配,尽管可以手动进行适配,但在使用过程中难免会遇到一些兼容性问题。

总体而言,这些缺点使得 TypeScript 不一定适合那些小型的、短期的个人项目。

TypeScript 的历史

下面简要介绍 TypeScript 的发展历程:

  • 2012 年,微软公司宣布推出 TypeScript 语言,其设计者是著名的编程语言设计大师 Anders Hejlsberg,他也是 C# 和 .NET 的设计师。微软推出这门语言的主要目的是让 JavaScript 程序员能够参与 Windows 8 应用程序的开发。当时,Windows 8 即将发布,其应用程序开发除了可以使用 C# 和 Visual Basic 外,还支持 HTML + JavaScript。微软希望 TypeScript 既能让 JavaScript 程序员快速上手,又能让 .Net 程序员感到熟悉。也就是说,TypeScript 的最初动机是降低 .NET 程序员的转移和学习成本,因此它的很多语法概念与 .NET 相似。此外,TypeScript 是一个开源项目,接受社区的参与,其核心编译器采用 Apache 2.0 许可证。微软希望通过这种方式,迅速提高这门语言在社区的接受度。
  • 2013 年,微软的 Visual Studio 2013 开始内置支持 TypeScript 语言。
  • 2014 年,TypeScript 1.0 版本发布,同年,代码仓库迁移到了 GitHub。
  • 2016 年,TypeScript 2.0 版本发布,引入了许多重大的语法功能。
  • 2018 年,TypeScript 3.0 版本发布。
  • 2020 年,TypeScript 4.0 版本发布。
  • 2023 年,TypeScript 5.0 版本发布。

如何学习

学习 TypeScript 必须先掌握 JavaScript 的语法,因为真正的实际功能是由 JavaScript 引擎完成的,TypeScript 只是添加了一个类型系统。本书假设读者已经熟悉 JavaScript 语言,因此不再介绍其语法,而是专注于 TypeScript 引入的新语法,主要是类型系统。如果你对 JavaScript 还不够熟悉,建议先阅读《JavaScript 教程》和《ES6 教程》,再阅读本书。

TypeScript 的类型系统

基本类型

概述

JavaScript 语言(注意,不是 TypeScript)将值分为 8 种类型:

  • boolean
  • string
  • number
  • bigint
  • symbol
  • object
  • undefined
  • null

TypeScript 继承了 JavaScript 的类型设计,这 8 种类型可以看作 TypeScript 的基本类型。需要注意的是,所有类型的名称都是小写字母,首字母大写的 NumberStringBoolean 等在 JavaScript 语言中是内置对象,而非类型名称。另外,undefinednull 既可以作为值,也可以作为类型,具体取决于使用场景。这 8 种基本类型是 TypeScript 类型系统的基础,复杂类型由它们组合而成。以下是对它们的简要介绍:

️boolean 类型

boolean 类型仅包含 truefalse 两个布尔值。

const x:boolean = true;
const y:boolean = false;

在上述示例中,变量 xy 属于 boolean 类型。

string 类型

string 类型包含所有字符串。

const x:string = 'hello';
const y:string = `${x} world`;

在这个示例中,普通字符串和模板字符串都属于 string 类型。

number 类型

number 类型包含所有整数和浮点数。

const x:number = 123;
const y:number = 3.14;
const z:number = 0xffff;

在上述示例中,整数、浮点数和非十进制数都属于 number 类型。

bigint 类型

bigint 类型包含所有的大整数。

const x:bigint = 123n;
const y:bigint = 0xffffn;

在这个示例中,变量 xy 属于 bigint 类型。需要注意的是,bigintnumber 类型不兼容。

const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错

在上述示例中,将 bigint 类型赋值为整数和小数都会报错。另外,bigint 类型是 ES2020 标准引入的,如果使用这个类型,TypeScript 编译的目标 JavaScript 版本不能低于 ES2020(即编译参数 target 不低于 es2020)。

symbol 类型

symbol 类型包含所有的 Symbol 值。

const x:symbol = Symbol();

在这个示例中,Symbol() 函数的返回值就是 symbol 类型。关于 symbol 类型的详细介绍,可参见《Symbol》一章。

object 类型

根据 JavaScript 的设计,object 类型包含了所有对象、数组和函数。

const x:object = { foo: 123 };
const y:object = [1, 2, 3];
const z:object = (n:number) => n + 1;

在上述示例中,对象、数组、函数都属于 object 类型。

⚫undefined 类型,null 类型

undefinednull 是两种独立的类型,它们各自只有一个值。undefined 类型只包含一个值 undefined,表示未定义(即还未给出定义,以后可能会有定义)。

let x:undefined = undefined;

在这个示例中,变量 x 属于 undefined 类型,第一个 undefined 是类型,第二个是值。null 类型也只包含一个值 null,表示为空(即此处没有值)。

const x:null = null;

在这个示例中,变量 x 属于 null 类型。需要注意的是,如果没有声明类型的变量被赋值为 undefinednull,在关闭编译设置 noImplicitAnystrictNullChecks 时,它们的类型会被推断为 any

// 关闭 noImplicitAny 和 strictNullChecks

let a = undefined;   // any
const b = undefined; // any

let c = null;        // any
const d = null;      // any

如果希望避免这种情况,则需要打开编译选项 strictNullChecks

// 打开编译设置 strictNullChecks

let a = undefined;   // undefined
const b = undefined; // undefined

let c = null;        // null
const d = null;      // null

在上述示例中,打开编译设置 strictNullChecks 后,赋值为 undefined 的变量会被推断为 undefined 类型,赋值为 null 的变量会被推断为 null 类型。

包装对象类型

包装对象的概念

JavaScript 的 8 种类型中,undefinednull 是两个特殊值,object 属于复合类型,剩下的五种属于原始类型(primitive value),代表最基本的、不可再分的值:

  • boolean
  • string
  • number
  • bigint
  • symbol

这五种原始类型的值都有对应的包装对象(wrapper object)。所谓“包装对象”,是指这些值在需要时会自动产生的对象。

'hello'.charAt(1) // 'e'

在这个示例中,字符串 hello 执行了 charAt() 方法。但在 JavaScript 语言中,只有对象才有方法,原始类型的值本身没有方法。这行代码能够运行,是因为在调用方法时,字符串会自动转为包装对象,charAt() 方法实际上是定义在包装对象上的。这种设计极大地方便了字符串处理,省去了将原始类型的值手动转换为对象实例的麻烦。

在五种包装对象中,symbol 类型和 bigint 类型无法直接获取它们的包装对象(即 Symbol()BigInt() 不能作为构造函数使用),但剩下的三种可以:

  • Boolean()
  • String()
  • Number()

以上三个构造函数执行后,可以直接获取某个原始类型值的包装对象。

const s = new String('hello');
typeof s // 'object'
s.charAt(1) // 'e'

在这个示例中,s 是字符串 hello 的包装对象,typeof 运算符返回 object,而非 string,但本质上它还是字符串,可以使用所有的字符串方法。需要注意的是,String() 只有当作构造函数使用时(即带有 new 命令调用),才会返回包装对象;如果当作普通函数使用(不带有 new 命令),返回的就是一个普通字符串。其他两个构造函数 Number()Boolean() 也是如此。

包装对象类型

包装对象的概念

JavaScript的8种类型中,undefinednull是两个特殊值,object属于复合类型,剩下的五种属于原始类型(primitive value),代表最基本的、不可再分的值:

  • boolean
  • string
  • number
  • bigint
  • symbol

这五种原始类型的值都有对应的包装对象(wrapper object)。所谓“包装对象”,是指这些值在需要时会自动产生的对象。

'hello'.charAt(1) // 'e'

在这个示例中,字符串hello执行了charAt()方法。但在JavaScript语言中,只有对象才有方法,原始类型的值本身没有方法。这行代码能够运行,是因为在调用方法时,字符串会自动转为包装对象,charAt()方法实际上是定义在包装对象上的。这种设计极大地方便了字符串处理,省去了将原始类型的值手动转换为对象实例的麻烦。

在五种包装对象中,symbol类型和bigint类型无法直接获取它们的包装对象(即Symbol()BigInt()不能作为构造函数使用),但剩下的三种可以:

  • Boolean()
  • String()
  • Number()

以上三个构造函数执行后,可以直接获取某个原始类型值的包装对象。

const s = new String('hello');
typeof s // 'object'
s.charAt(1) // 'e'

在这个示例中,s是字符串hello的包装对象,typeof运算符返回object,而非string,但本质上它还是字符串,可以使用所有的字符串方法。需要注意的是,String()只有当作构造函数使用时(即带有new命令调用),才会返回包装对象;如果当作普通函数使用(不带有new命令),返回的就是一个普通字符串。其他两个构造函数Number()Boolean()也是如此。

包装对象类型与字面量类型

由于包装对象的存在,每个原始类型的值都有包装对象和字面量两种情况。

'hello' // 字面量
new String('hello') // 包装对象

上面示例中,第一行是字面量,第二行是包装对象,它们都是字符串。

为了区分这两种情况,TypeScript对五种原始类型分别提供了大写和小写两种类型:

  • Boolean和boolean
  • String和string
  • Number和number
  • BigInt和bigint
  • Symbol和symbol

其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。

const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确
const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错

上面示例中,String类型可以赋值为字符串的字面量,也可以赋值为包装对象。但是,string类型只能赋值为字面量,赋值为包装对象就会报错。

建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。而且,TypeScript把很多内置方法的参数,定义成小写类型,使用大写类型会报错。

const n1:number = 1;
const n2:Number = 1;
Math.abs(n1) // 1
Math.abs(n2) // 报错

上面示例中,Math.abs()方法的参数类型被定义成小写的number,传入大写的Number类型就会报错。

上一小节说过,Symbol()BigInt()这两个函数不能当作构造函数使用,所以没有办法直接获得symbol类型和bigint类型的包装对象,除非使用下面的写法。但是,它们没有使用场景,因此SymbolBigInt这两个类型虽然存在,但是完全没有使用的理由。

let a = Object(Symbol());
let b = Object(BigInt());

上面示例中,得到的就是Symbol和BigInt的包装对象,但是没有使用的意义。

注意,目前在TypeScript里面,symbolSymbol两种写法没有差异,bigintBigInt也是如此,不知道是否属于官方的疏忽。建议始终使用小写的symbolbigint,不使用大写的SymbolBigInt

Object类型与object类型

Object类型

大写的Object类型代表JavaScript语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。

let obj:Object;
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

上面示例中,原始类型值、对象、数组、函数都是合法的Object类型。

事实上,除了undefinednull这两个值不能转为对象,其他任何值都可以赋值给Object类型。

let obj:Object;
obj = undefined; // 报错
obj = null; // 报错

上面示例中,undefinednull赋值给Object类型,就会报错。

另外,空对象{}Object类型的简写形式,所以使用Object时常常用空对象代替。

let obj:{};
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;

上面示例中,变量obj的类型是空对象{},就代表Object类型。

显然,无所不包的Object类型既不符合直觉,也不方便使用。

object类型

小写的object类型代表JavaScript里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。

let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错

上面示例中,object类型不包含原始类型值,只包含对象、数组和函数。

大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型object,不使用大写类型Object

注意,无论是大写的Object类型,还是小写的object类型,都只包含JavaScript内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。

const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };
o1.toString() // 正确
o1.foo // 报错
o2.toString() // 正确
o2.foo // 报错

上面示例中,toString()是对象的原生方法,可以正确访问。foo是自定义属性,访问就会报错。如何描述对象的自定义属性,详见《对象类型》一章。

⚫undefined和null的特殊性

undefinednull既是值,又是类型。

作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为undefinednull

let age:number = 24;
age = null;      // 正确
age = undefined; // 正确

上面代码中,变量age的类型是number,但是赋值为nullundefined并不报错。

这并不是因为undefinednull包含在number类型里面,而是故意这样设计,任何类型的变量都可以赋值为undefinednull,以便跟JavaScript的行为保持一致。

JavaScript的行为是,变量如果等于undefined就表示还没有赋值,如果等于null就表示值为空。所以,TypeScript就允许了任何类型的变量都可以赋值为这两个值。

但是有时候,这并不是开发者想要的行为,也不利于发挥类型系统的优势。

const obj:object = undefined;
obj.toString() // 编译不报错,运行就报错

上面示例中,变量obj等于undefined,编译不会报错。但是,实际执行时,调用obj.toString()就报错了,因为undefined不是对象,没有这个方法。

为了避免这种情况,及早发现错误,TypeScript提供了一个编译选项strictNullChecks。只要打开这个选项,undefinednull就不能赋值给其他类型的变量(除了any类型和unknown类型)。

下面是tsc命令打开这个编译选项的例子。

// tsc --strictNullChecks app.ts
let age:number = 24;
age = null;      // 报错
age = undefined; // 报错

上面示例中,打开--strictNullChecks以后,number类型的变量age就不能赋值为undefinednull

这个选项在配置文件tsconfig.json的写法如下。

{
  "compilerOptions": {
    "strictNullChecks": true
    // ...
  }
}

打开strictNullChecks以后,undefinednull这两种值也不能互相赋值了。

// 打开strictNullChecks
let x:undefined = null; // 报错
let y:null = undefined; // 报错

上面示例中,undefined类型的变量赋值为null,或者null类型的变量赋值为undefined,都会报错。

总之,打开strictNullChecks以后,undefinednull只能赋值给自身,或者any类型和unknown类型的变量。

let x:any     = undefined;
let y:unknown = null;

值类型

TypeScript规定,单个值也是一种类型,称为“值类型”。

let x:'hello';
x = 'hello'; // 正确
x = 'world'; // 报错

上面示例中,变量x的类型是字符串hello,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。

TypeScript推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。

// x的类型是"https"
const x = 'https';
// y的类型是string
const y:string = 'https';

上面示例中,变量xconst命令声明的,TypeScript就会推断它的类型是值https,而不是string类型。

这样推断是合理的,因为const命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。

注意,const命令声明的变量,如果赋值为对象,并不会推断为值类型。

// x的类型是{ foo: number }
const x = { foo: 1 };

上面示例中,变量x没有被推断为值类型,而是推断属性foo的类型是number。这是因为JavaScript里面,const变量赋值为对象时,属性值是可以改变的。

值类型可能会出现一些很奇怪的报错。

const x:5 = 4 + 1; // 报错

上面示例中,等号左侧的类型是数值5,等号右侧4 + 1的类型,TypeScript推测为number。由于5number的子类型,number5的父类型,父类型不能赋值给子类型,所以报错了(详见本章后文)。

但是,反过来是可以的,子类型可以赋值给父类型。

let x:5 = 5;
let y:number = 4 + 1;
x = y; // 报错
y = x; // 正确

上面示例中,变量x属于子类型,变量y属于父类型。子类型x不能赋值为父类型y,但是反过来是可以的。

如果一定要让子类型可以赋值为父类型的值,就要用到类型断言(详见《类型断言》一章)。

const x:5 = (4 + 1) as 5; // 正确

上面示例中,在4 + 1后面加上as 5,就是告诉编译器,可以把4 + 1的类型视为值类型5,这样就不会报错了。

只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。

联合类型

联合类型(union types)指的是多个类型组成的一个新类型,使用符号|表示。

联合类型A|B表示,任何一个类型只要属于AB,就属于联合类型A|B

let x:string|number;
x = 123; // 正确
x = 'abc'; // 正确

上面示例中,变量x就是联合类型string|number,表示它的值既可以是字符串,也可以是数值。

联合类型可以与值类型相结合,表示一个变量的值有若干种可能。

let setting:true|false;
let gender:'male'|'female';
let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';

上面的示例都是由值类型组成的联合类型,非常清晰地表达了变量的取值范围。其中,true|false其实就是布尔类型boolean

前面提到,打开编译选项strictNullChecks后,其他类型的变量不能赋值为undefinednull。这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法。

let name:string|null;
name = 'John';
name = null;

上面示例中,变量name的值可以是字符串,也可以是null

联合类型的第一个成员前面,也可以加上竖杠|,这样便于多行书写。

let x:
  | 'one'
  | 'two'
  | 'three'
  | 'four';

上面示例中,联合类型的第一个成员one前面,加上了竖杠。

如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(type narrowing),区分该值到底属于哪一种类型,然后再进一步处理。

function printId(
  id:number|string
) {
    console.log(id.toUpperCase()); // 报错

上面示例中,参数变量id可能是数值,也可能是字符串,这时直接对这个变量调用toUpperCase()方法会报错,因为这个方法只存在于字符串,不存在于数值。

解决方法就是对参数id做一下类型缩小,确定它的类型以后再进行处理。

function printId(
  id:number|string
) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase());
  } else {
    console.log(id);
  }

上面示例中,函数体内部会判断一下变量id的类型,如果是字符串,就对其执行toUpperCase()方法。

“类型缩小”是TypeScript处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。

下面是“类型缩小”的另一个例子。

function getPort(
  scheme: 'http'|'https'
) {
  switch (scheme) {
    case 'http':
      return 80;
    case 'https':
      return 443;
  }

上面示例中,函数体内部对参数变量scheme进行类型缩小,根据不同的值类型,返回不同的结果。

交叉类型

交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&表示。

交叉类型A&B表示,任何一个类型必须同时属于AB,才属于交叉类型A&B,即交叉类型同时满足AB的特征。

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。如此一来,在使用类型时,Age就和number具有相同的功能,能让代码表意更加清晰。比如在一个处理用户信息的函数中,如果年龄字段使用Age作为类型,相较于number,能更直观地让阅读代码的人明白该字段代表的含义。

别名不仅能增强代码可读性,还能让复杂类型的使用更加便捷,并且在后续需要修改变量类型时,仅调整别名定义处即可,无需在大量引用处逐个修改,有效降低了维护成本。

需要注意的是,别名不允许重名。

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别名,最终Greeting的实际类型为"hello world"

值得一提的是,type命令属于与类型相关的代码,在编译成JavaScript时会被全部删除,它仅在TypeScript的类型检查和代码编写阶段发挥作用。

typeof运算符

在JavaScript语言里,typeof运算符是一元运算符,其作用是返回一个字符串,代表操作数的类型。

typeof 'foo'; // 'string'

上述示例中,typeof运算符返回字符串foo的类型为string。这里的typeof操作数是一个值。在JavaScript中,typeof运算符只会返回八种结果,且都是字符串。

typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n // "bigint"

而在TypeScript中,typeof运算符被扩展到了类型运算领域。它的操作数依然是一个值,但返回的不再是字符串,而是该值对应的TypeScript类型。

const a = { x: 0 };
type T0 = typeof a;   // { x: number }
type T1 = typeof a.x; // number

在这个示例中,typeof a返回的是变量a的TypeScript类型{ x: number }typeof a.x返回的则是属性x的类型number

这种用于类型运算的typeof,只能出现在和类型相关的代码中,不能用于值运算。也就是说,同一代码中可能存在两种typeof运算符,一种用于JavaScript代码的值运算,另一种用于TypeScript代码的类型运算,使用时切勿混淆。

let a = 1;
let b:typeof a;
if (typeof a === 'number') {
  b = a;
}

在这段代码里,第一个typeof用于类型运算,确定a的类型;第二个typeof用于值运算,判断a的值的类型是否为number。它们遵循不同的规则,编译后,用于类型运算的typeof会被删除,只保留值运算的typeof。上述代码的编译结果如下:

let a = 1;
let b;
if (typeof a === 'number') {
    b = a;
}

此外,TypeScript规定,typeof的参数只能是标识符,不能是需要运算的表达式。

type T = typeof Date(); // 报错

该示例报错的原因是Date()是一个需要运算才能得到结果的表达式,不符合typeof参数的要求。

同时,typeof命令的参数不能是类型。

type Age = number;
type MyAge = typeof Age; // 报错

在此例中,Age是一个类型别名,将其作为typeof命令的参数会引发报错。

typeof是TypeScript中非常重要的运算符,当开发者不清楚某个变量的类型时,使用typeof foo就能获取其类型,为代码编写和类型检查提供了便利。

块级类型声明

TypeScript支持块级类型声明,即类型可以在代码块(由大括号表示)内声明,且仅在当前代码块内有效。

if (true) {
  type T = number;
  let v:T = 5;
} else {
  type T = string;
  let v:T = 'hello';
}

在上述示例中,存在两个代码块,每个代码块都声明了类型T。这两个T声明仅在各自的代码块内有效,在代码块外部无法使用。这种特性在一些特定场景下非常有用,比如在一个复杂的函数内部,不同的逻辑分支可能需要使用相同名称但不同类型的变量,块级类型声明就能避免命名冲突,让代码结构更加清晰。

类型的兼容

在TypeScript中,类型之间存在兼容关系,某些类型可以兼容其他类型。

type T = number|string;
let a:number = 1;
let b:T = a;

在这个例子里,变量ab的类型不同,但将a赋值给b并不会报错。这种情况下,我们称b的类型兼容a的类型。

TypeScript为此定义了专门术语:如果类型A的值可以赋值给类型B,那么类型A就是类型B的子类型(subtype)。在上例中,number类型就是number|string类型的子类型。

TypeScript遵循这样一条规则:凡是可以使用父类型的地方,都可以使用子类型,但反之则不行。

let a:'hi' = 'hi';
let b:string = 'hello';
b = a; // 正确
a = b; // 报错

在此示例中,histring的子类型,stringhi的父类型。所以变量a可以赋值给变量b,而将变量b赋值给变量a就会报错。

之所以有这样的规则,是因为子类型继承了父类型的所有特征,所以能够在父类型适用的场合使用;但子类型可能具有一些父类型没有的独特特征,因此父类型不能用于子类型的场合。理解并合理运用类型兼容关系,有助于编写更健壮、灵活的TypeScript代码。

你可能感兴趣的:(bolg,typescript,javascript,前端)