Typescript入门-类型讲解
- 前言
- 静态类型的优点
- 静态类型的缺点
- 值与类型
- TypeScript Playground
- TypeScript 的编译
- 类型声明
- 类型推断
- any 类型
- 类型推断问题
- 污染问题
- TypeScript 的类型系统
- 基本类型概述
- 值类型
- 联合类型
- 交叉类型
- type 命令
- typeof 运算符
本文对 TypeScript 常用知识点进行入门讲解,以满足初学者对基础项目进行开发。
前言
TypeScript 简称 TS,是微软公司开发的一种基于 JavaScript 的编程语言,可以说是 JavaScript 语言的超集,增强 JavaScript 的功能,使得更适合用在多人合作的企业级项目。
TS 的静态类型系统可以避免很多问题,这也是为什么 TS 适合用在多人协作的复杂项目的原因,也是越来越多前端开源项目支持 TS 的最主要原因。
TS 对 JavaScript 添加的最主要部分,就是一个独立的类型系统。可以这样理解,类型是人为添加的一种编程约束和用法提示。主要目的是在软件开发过程中,为编译器和开发工具提供更多的验证和帮助,帮助提高代码质量,减少错误。
静态类型的优点
- 更好的 IDE 支持,做到语法提示和自动补全。
- 有利于代码的静态分析。有了静态类型,不必运行代码,就可以确定变量的类型,从而推断代码有没有错误。
- 有利于发现错误。由于每个值、每个变量、每个运算符都有严格的类型约束,TypeScript 就能轻松发现拼写错误、语义错误和方法调用错误,节省程序员的时间。
- 提供了代码文档。类型信息可以部分替代代码文档,借助类型信息,很多工具能够直接生成文档。
- 有助于代码重构。一般来说,只要函数或对象的参数和返回值保持类型不变,就能基本确定,重构后的代码也能正常运行。
静态类型的缺点
- 更高的学习成本。
- 增加了编程工作量。
- 丧失了动态类型的代码灵活性。
- 引入了独立的编译步骤。原生的 JavaScript 代码,可以直接在 JavaScript 引擎运行。添加类型系统以后,就多出了一个单独的编译步骤,检查类型是否正确,并将 TS 代码转成 JavaScript 代码,这样才能运行。
- 兼容性问题。TS 依赖 JavaScript 生态,需要用到很多外部模块。但是,过去大部分 JavaScript 项目都没有做 TS 适配,虽然可以自己动手做适配,不过使用时难免还是会有一些兼容性问题。
总的来说,这些缺点使得 TS 不一定适合那些小型的、短期的个人项目。
值与类型
学习 TypeScript 需要分清楚“值”(value)和“类型”(type)。
“类型”是针对“值”的,可以视为是后者的一个元属性。每一个值在 TypeScript 里面都是有类型的。比如,3是一个值,它的类型是number。
TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成。
这一点务必牢记。TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。前者使用 JavaScript 语法,后者使用 TypeScript 的类型语法。
它们是可以分离的,TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。
编写 TypeScript 项目时,不要混淆哪些是值代码,哪些是类型代码。
TypeScript Playground
最简单的 TypeScript 使用方法,就是使用官网的在线编译页面,叫做 TypeScript Playground。
只要打开这个网页,把 TypeScript 代码贴进文本框,它就会在当前页面自动编译出 JavaScript 代码,还可以在浏览器执行编译产物。如果编译报错,它也会给出详细的报错信息。
这个页面还具有支持完整的 IDE 支持,可以自动语法提示。此外,它支持把代码片段和编译器设置保存成 URL,分享给他人。
本文的示例都建议放到这个页面,进行查看和编译。
TypeScript 的编译
JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码。所以,TypeScript 项目要想运行,必须先转为 JavaScript 代码,这个代码转换的过程就叫做“编译”(compile)。
TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。
因此,TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。
TypeScript 官方提供的编译器叫做 tsc,可以将 TypeScript 脚本编译成 JavaScript 脚本。本机想要编译 TypeScript 代码,必须安装 tsc。
根据约定,TypeScript 脚本文件使用 .ts
后缀名,JavaScript 脚本文件使用 .js 后缀名。tsc 的作用就是把 .ts
脚本转变成 .js
脚本。
tsc 是一个 npm 模块,使用下面的命令安装(必须先安装 npm)。
npm install -g typescript
安装完成后,检查一下是否安装成功。
# 或者 tsc --version
tsc -v
Version 5.2.2
安装 tsc 之后,就可以编译 TypeScript 脚本了。
tsc命令后面,加上 TypeScript 脚本文件,就可以将其编译成 JavaScript 脚本。
tsc app.ts
上面命令会在当前目录下,生成一个app.js脚本文件,这个脚本就完全是编译后生成的 JavaScript 代码。
编译过程中,如果没有报错,tsc命令不会有任何显示。所以,如果你没有看到任何提示,就表示编译成功了。
如果编译报错,tsc命令就会显示报错信息,但是这种情况下,依然会编译生成 JavaScript 脚本。
这是因为 TypeScript 团队认为,编译器的作用只是给出编译错误,至于怎么处理这些错误,那就是开发者自己的判断了。开发者更了解自己的代码,所以不管怎样,编译产物都会生成,让开发者决定下一步怎么处理。
TypeScript 允许将tsc的编译参数,写在配置文件tsconfig.json。只要当前目录有这个文件,tsc就会自动读取,所以运行时可以不写参数。
tsc file1.ts file2.ts --outFile dist/app.js
上面这个命令写成tsconfig.json,就是下面这样。
{"files": ["file1.ts", "file2.ts"],"compilerOptions": {"outFile": "dist/app.js"}
}
有了这个配置文件,编译时直接调用tsc命令就可以了。
tsc
类型声明
TypeScript 代码最明显的特征,就是为 JavaScript 变量加上了类型声明。
let foo: string;
上面示例中,变量foo的后面使用冒号,声明了它的类型为 string。
类型声明的写法,一律为在标识符后面添加“冒号 + 类型”。函数参数和返回值,也是这样来声明类型。
function numToString(num: number): string {return String(num);
}
numToString(123)
上面示例中,函数 numToString() 的参数 num 的类型是 number。参数列表的圆括号后面,声明了返回值的类型是 string。
注意,变量的值应该与声明的类型一致,如果不一致,TypeScript 就会报错。
// 报错
let foo: string = 123;
上面示例中,变量 foo 的类型是字符串,但是赋值为数值 123,TypeScript 就报错了。
另外,TypeScript 规定,变量只有赋值后才能使用,否则就会报错。
let x: number;
console.log(x); // 报错
上面示例中,变量 x 没有赋值就被读取,导致报错。而 JavaScript 允许这种行为,不会报错,没有赋值的变量会返回undefined。
类型推断
类型声明并不是必需的,如果没有,TypeScript 会自己推断类型。
let foo = 123;
上面示例中,变量foo并没有类型声明,TypeScript 就会推断它的类型。由于它被赋值为一个数值,因此 TypeScript 推断它的类型为number。
后面,如果变量foo更改为其他类型的值,跟推断的类型不一致,TypeScript 就会报错。
let foo = 123;
foo = "hello"; // 报错
上面示例中,变量foo的类型推断为number,后面赋值为字符串,TypeScript 就报错了。
TypeScript 也可以推断函数的返回值。
function numToString(num: number) {return String(num);
}
上面示例中,函数 numToString() 没有声明返回值的类型,但是 TypeScript 推断返回的是字符串。正是因为 TypeScript 的类型推断,所以函数返回值的类型通常是省略不写的。
从这里可以看到,TypeScript 的设计思想是,类型声明是可选的,你可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。由于这个原因。所有 JavaScript 代码都是合法的 TypeScript 代码。
这样设计还有一个好处,将以前的 JavaScript 项目改为 TypeScript 项目时,你可以逐步地为老代码添加类型,即使有些代码没有添加,也不会无法运行。
any 类型
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
let x: any;x = 1; // 正确
x = "foo"; // 正确
x = true; // 正确
上面示例中,变量x的类型是any,就可以被赋值为任意类型的值。
变量类型一旦设为any,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。
let x: any = "hello";x(1); // 不报错
x.foo = 100; // 不报错
上面示例中,变量x的值是一个字符串,但是把它当作函数调用,或者当作对象读取任意属性,TypeScript 编译时都不报错。原因就是x的类型是any,TypeScript 不对其进行类型检查。由于这个原因,应该尽量避免使用any类型,否则就失去了使用 TypeScript 的意义。
实际开发中,any类型主要适用以下两个场合。
(1)出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为any。
(2)为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为any。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上any,TypeScript 编译时就不会报错。
总之,TypeScript 认为,只要开发者使用了 any 类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。
从集合论的角度看,any 类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。
类型推断问题
对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any。
function add(x, y) {return x + y;
}add(1, [1, 2, 3]); // 不报错
上面示例中,函数add()的参数变量x和y,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是any。以至于后面就不再对函数add()进行类型检查了,怎么用都可以。
这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any。
污染问题
any类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。
let x: any = "hello";
let y: number;y = x; // 不报错y * 123; // 不报错
y.toFixed(); // 不报错
上面示例中,变量x的类型是any,实际的值是一个字符串。变量y的类型是number,表示这是一个数值变量,但是它被赋值为x,这时并不会报错。然后,变量y继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。
污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用any类型的另一个主要原因。
TypeScript 的类型系统
TypeScript 继承了 JavaScript 的类型,在这个基础上,定义了一套自己的类型系统。
基本类型概述
JavaScript 语言(注意,不是 TypeScript)将值分成 8 种类型。
- number
- string
- boolean
- null
- undefined
- bigint
- symbol
- object
TypeScript 继承了 JavaScript 的类型设计,以上 8 种类型可以看作 TypeScript 的基本类型。
注意,上面所有类型的名称都是小写字母,首字母大写的 Number、String、Boolean 等在 JavaScript 语言中都是内置对象,而不是类型名称。
另外,undefined 和 null 既可以作为值,也可以作为类型,取决于在哪里使用它们。
这 8 种基本类型是 TypeScript 类型系统的基础,复杂类型由它们组合而成。
以下是它们的简单介绍。
-
number 类型
number类型包含所有整数和浮点数。
const x: number = 123; const y: number = 3.14; const z: number = 0xffff;
上面示例中,整数、浮点数和非十进制数都属于 number 类型。
-
string 类型
string类型包含所有字符串。
const x: string = "hello"; const y: string = `${x} world`;
上面示例中,普通字符串和模板字符串都属于 string 类型。
-
boolean 类型
boolean类型只包含 true 和 false 两个布尔值。
const x: boolean = true; const y: boolean = false;
上面示例中,变量x和y就属于 boolean 类型。
-
bigint 类型
bigint 类型包含所有的大整数。
const x: bigint = 123n; const y: bigint = 0xffffn;
上面示例中,变量x和y就属于 bigint 类型。
bigint 与 number 类型不兼容。
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 类型。
-
object 类型
根据 JavaScript 的设计,object 类型包含了所有对象、数组和函数。
const x: object = { foo: 123 }; const y: object = [1, 2, 3]; const z: object = (n: number) => n + 1;
上面示例中,对象、数组、函数都属于 object 类型。
-
undefined 类型,null 类型
undefined 和 null 是两种独立类型,它们各自都只有一个值。
undefined 类型只包含一个值undefined,表示未定义(即还未给出定义,以后可能会有定义)。
let x: undefined = undefined;
上面示例中,变量x就属于 undefined 类型。两个undefined里面,第一个是类型,第二个是值。
null 类型也只包含一个值null,表示为空(即此处没有值)。
const x: null = null;
上面示例中,变量x就属于 null 类型。
注意,如果没有声明类型的变量,被赋值为undefined或null,它们的类型会被推断为any。
let a = undefined; // any let b = null; // any
如果希望避免这种情况,则需要打开编译选项 strictNullChecks。
// 打开编译设置 strictNullChecks let a = undefined; // undefined const b = null; // null
上面示例中,打开编译设置 strictNullChecks 以后,赋值为undefined的变量会被推断为undefined类型,赋值为null的变量会被推断为null类型。
值类型
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 };
上面示例中,变量x没有被推断为值类型,而是推断属性foo的类型是number。这是因为 JavaScript 里面,const变量赋值为对象时,属性值是可以改变的。
值类型可能会出现一些很奇怪的报错。
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; // 正确
上面示例中,变量x属于子类型,变量y属于父类型。y不能赋值为子类型x,但是反过来是可以的。
如果一定要让父类型的值赋值为子类型,就要用到类型断言。
const x: 5 = (4 + 1) as 5; // 正确
上面示例中,在 4 + 1 后面加上 as 5,就是告诉编译器,可以把 4 + 1 的类型视为值类型5,这样就不会报错了。
只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。
联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号 |
表示。
联合类型 A|B
表示,任何一个类型只要属于A或B,就属于联合类型 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
后,其他类型的变量不能赋值为 undefined 或 null。这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法。
let name: string | null;name = "John";
name = null;
上面示例中,变量 name 的值可以是字符串,也可以是 null。
如果一个变量有多种类型,读取该变量时,往往需要进行“类型缩小”(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
表示,任何一个类型必须同时属于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 World = "world";
type Greeting = `hello ${World}`;
上面示例中,别名Greeting使用了模板字符串,读取另一个别名World。
type 命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。
typeof 运算符
JavaScript 语言中,typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型。
typeof "foo"; // 'string'
上面示例中,typeof运算符返回字符串foo的类型是string。
注意,这时 typeof 的操作数是一个值。
JavaScript 里面,typeof运算符只可能返回八种结果,而且都是字符串。
typeof 1337; // "number"
typeof "foo"; // "string"
typeof true; // "boolean"
typeof {}; // "object"
typeof undefined; // "undefined"
typeof 127n; // "bigint"
typeof Symbol(); // "symbol"
typeof parseInt; // "function"
上面示例是 typeof 运算符在 JavaScript 语言里面,可能返回的八种结果。
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返回的是 TypeScript 类型,所以只能用在类型运算之中(即跟类型相关的代码之中),不能用在值运算。
也就是说,同一段代码可能存在两种typeof运算符,一种用在值相关的 JavaScript 代码部分,另一种用在类型相关的 TypeScript 代码部分。
let a = 1;
let b: typeof a;if (typeof a === "number") {b = a;
}
上面示例中,用到了两个typeof,第一个是类型运算,第二个是值运算。它们是不一样的,不要混淆。
JavaScript 的 typeof 遵守 JavaScript 规则,TypeScript 的 typeof 遵守 TypeScript 规则。它们的一个重要区别在于,编译后,前者会保留,后者会被全部删除。
上例的代码编译结果如下。
let a = 1;
let b;
if (typeof a === "number") {b = a;
}
上面示例中,只保留了原始代码的第二个 typeof,删除了第一个 typeof。
由于编译时不会进行 JavaScript 的值运算,所以 TypeScript 规定,typeof 的参数只能是标识符,不能是需要运算的表达式。
type T = typeof Date(); // 报错
上面示例会报错,原因是 typeof 的参数不能是一个值的运算式,而Date()需要运算才知道结果。
另外,typeof命令的参数不能是类型。
type Age = number;
type MyAge = typeof Age; // 报错
上面示例中,Age是一个类型别名,用作typeof命令的参数就会报错。
typeof 是一个很重要的 TypeScript 运算符,有些场合不知道某个变量foo的类型,这时使用typeof foo就可以获得它的类型。