TypeScript 入门
JS 的缺点:
- 报错少,后期维护很麻烦
- 变量类型是动态的(弱类型语言),可能带来安全隐患
TypeScript 是什么?
- 以 JavaScript 为基础构建的语言
- 一个 JavaScript 的超集(完全支持 JS,且包含更多功能)
- 扩展了 JS 并添加了类型(强类型语言)
- 可以在任何支持 JavaScript 的平台中执行
TS 不能被 JS 解析器直接执行,需要把 .ts 编译为 .js,然后才能被浏览器执行
TS 在编译阶段就能找出一些错误。
TS 增加了什么?
- 类型(元组,枚举等)以及类型安全
- 支持 ES 的新特性,添加 ES 不具备的新特性
- 丰富的配置选项(可以被编译为任意版本的 JS,类似于 babel,解决兼容性问题)
- 强大的开发工具(提高开发效率)
总结来说:TS 不仅是一门语言,更是生产力工具。
开发环境搭建
- 安装 Node.js
- 全局安装 typescript
npm i -g typescript - 创建 ts 文件
- 使用 tsc 将 ts 文件进行编译
tsc xxx.ts
类型
基本类型
类型
| 类型(小写) | 例子 | 描述 | |
|---|---|---|---|
| number | bigint | 1, -33, 2.5; 233n | 任意数字 |
| string | 'h1', "hi", hi | 任意字符串 | |
| boolean | true, false | 布尔值 | |
| 字面量 | 其本身 | 限制变量的值就是该字面量的值 | |
| any | * | 任意类型 | |
| unknown | * | 类型安全的 any,unknown 类型的变量不能直接赋值给其他变量(除了 unknown 和 any) | |
| void | 空值 (undefined | null) | 常用于函数返回值类型声明 |
| never | 没有值 | 不能是任何值(比如函数没有返回值) | |
| object | {name: '孙悟空'} | 任意的 JS 对象 | |
| array | [1, 2, 3] | 任意的 JS 数组 | |
| tuple | [4, 5] | 元组,TS新增类型,固定长度的数组 | |
| enum | enum{A, B} | 枚举,TS新增类型 |
类型声明
-
类型声明是 TS 非常重要的一个特点
-
通过类型声明可以指定 TS 变量(参数、形参)的类型
-
指定类型后,当为变量赋值时,TS 编译器会自动检查值是否符合类型声明,符合则赋值,否则报错(默认情况下仍然会转换为 js)
-
语法:
// let 变量: 类型; let a: number; a: number = 233; // let 变量: 类型 = 值; let b: boolean = false; // 声明和赋值同时进行,可以简写,会自动判断类型 let c = false; // 参数和返回值的类型声明 function (d: number, e: string): string{ }
自动类型判断
- TS 拥有自动的类型判断机制
- 当对变量的声明和赋值是同时进行的,TS编译器会自动判断变量的类型
- 所以如果变量的声明和赋值是同时进行的,可以省略掉类型声明
// 字面量类型声明
let f: 10;
// any,不建议使用
let the_any: any; // 相当于关闭了 ts 的类型检测
let the_any2; // 隐式 any
the_any = 666;
gender = the_any; // 不会报错!!
// unknown 表示未知类型的值
let the_unknown: unknown;
the_unknown = "666";
gender = the_unknown; // 报错,unknown 和 string 类型不同
let s: string;
if (typeof the_unknown === "string") {
s = the_unknown; // 不报错
}
非空断言
紧跟在变量名之后的 !, 表示这个变量一定不是 undefined 或者 null
数组
类型[]Array<类型>
let str_arr: string[]; // 字符串数组
let num_arr: number[]; // 数值数组
let num_arr2: Array<number>; // 数值数组
let any_arr: Array<any>; // 任意类型数组,尽量不要使用
元组
[类型, 类型, ...]
长度固定的数组
let t: [string, string]; // 只有两个字符串的元组
枚举
把可能的情况列举出来,值实际上是数值
enum Gender{
Male, // 实际上是 0 (默认从 0 开始)
Female // 实际上是 1
}
let i: {name: string, gender: Gender};
i = {
name: '孙悟空',
gender: Gender.Male
}
编译前(.ts)
enum Fruit {
apple = 1, // 1
banana, // 2
orange = 100, // 100
pear, // 101
}
console.log(Fruit[1], Fruit["apple"]); // apple 1
编译后(.js)
var Fruit;
(function (Fruit) {
Fruit[Fruit["apple"] = 1] = "apple";
Fruit[Fruit["banana"] = 2] = "banana";
Fruit[Fruit["orange"] = 100] = "orange";
Fruit[Fruit["pear"] = 101] = "pear";
})(Fruit || (Fruit = {}));
console.log(Fruit[1], Fruit["apple"]);
函数类型
参数 / 返回值的类型
JS 中,传入函数的参数的类型/数量不是固定的,可能造成意想不到的错误
function sum(a, b) {
return a + b;
}
console.log(sum(123, 456)); // 579
console.log(sum(123, '456')); // '123456'
TS:在参数后可以指定类型,在函数后可以指定返回值类型
function sum(a: number, b: number): number {
return a + b;
}
返回空值 / 没有返回值
// 返回空值(undefined | null)
function fn(arg1: number, arg2: string): void {
// ...
}
// never 永远没有返回值
function fn2(): never {
throw new Error('报错了!');
}
箭头函数指定类型
语法: (形参:类型, 形参:类型 ... ) => 返回值
// 用箭头函数设置函数结构类型声明
// 语法: (形参:类型, 形参:类型 ... ) => 返回值
let e: (a: number, b: number) => number;
e = function (n1, n2) { // 会自动限定类型
return n1 + n2;
}
面向对象
接口
描述一个对象的类型
描述函数参数的类型,其中的属性不作顺序要求,且可以传入除了接口里定义的属性之外的属性。
interface LabelledValue {
label: string;
}
// function printLabel(labelledObj: { label: string })
function printLabel( labelledObj: LabelledValue ) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj); // Size 10 Object
// printLabel({ size: 233, label: "new Label" }); // error! no size
可选属性
在属性名之后加 ? ,表示这些属性不是必须的
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({ color: "black" });
console.log(mySquare); // {color: 'black', area: 100}
只读属性
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // error!
// 只读的数组
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
// ro[0] = 12; // error!
// ro.push(5); // error!
// ro.length = 100; // error!
// a = ro; // error!
a = ro as number[]; // right!
readonly vs const
最简单判断方法是:
- 后面跟的值要用作变量:使用
const - 后面跟的值要用作属性:使用
readonly
函数类型
给接口定义一个调用签名,然后用接口表示函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
- 参数不一定要同名,只要参数按顺序满足类型即可。
- 函数声明时参数可以不指定类型,ts 会自动推断出类型
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
可索引的类型
实现自定义属性
比如:指定 a[10] 或 ageMap["deniel"] 的类型
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
interface RandomKey {
[propName: string]: string
}
const obj: RandomKey = {
a: "1",
b: "2",
c: "3"
}
共支持两种索引签名:字符串和数字。
两者可以共用,但是必须保证数字索引的返回值必须是字符串索引返回值类型的子类型。这是因为当使用
number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。
类
TS 中的类与 JS 中的类写法基本一致,不过增加了新的功能
类的类型
使用 implements 让接口约束 类 的规范,也可以描述类中的方法。
接口只描述类的公共部分,而不会描述私有部分,不会检查类是否具有某些私有成员。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
当一个类实现(implement)了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
访问控制修饰符
public、protected、private,与 C++ 中基本一致。
即:
public(默认) 可以在类、类的实例、继承类、继承类的实例、外部中调用protected 可以在类、类的实例、继承类、继承类的实例中调用private 可以在类、类的实例中调用
class Person {
public id: number;
protected name: string;
private gender: string;
public constructor(name: string) {
this.id = 233;
this.name = name;
this.gender = 'male';
}
}
class Student extends Person {
study() {
console.log(this.name);
console.log(this.gender); // Property 'gender' is private and only accessible within class 'Person'.ts(2341)
}
}
let person = new Person("xiaoming");
person.id;
person.name; // Property 'name' is protected and only accessible within class 'Person' and its subclasses.ts(2445)
person.gender; // Property 'gender' is private and only accessible within class 'Person'.ts(2341)
抽象类
使用 abstract 关键字
与 C++ 基本一致,抽象类只能被继承(用于表示一个抽象的概念),不能被实例化。
抽象方法(类似于 C++ 中的虚函数)必须被子类实现。
abstract class Animal {
constructor(name: string) {
this.name = name;
}
public name: string;
public abstract sayHi(): void;
}
class Dog extends Animal { // Non-abstract class 'Dog' does not implement inherited abstract member 'sayHi' from class 'Animal'.
constructor(name: string) {
super(name);
}
}
私有属性
#
TypeScript 进阶
高级类型
联合类型
| 表示可以是多种类型中的一种
// 管道符 |
let gender: "male" | "female"; // 表示二选一
let c: boolean | number; // 联合类型
gender = "male";
gender = "female";
c = true;
c = 233;
交叉类型
& 表示必须同时符合各个类型的约束
interface Person {
name: string;
id: number;
}
type Student = Person & { grade: number };
const stu: Student;
// 此时 stu 这个对象就包含 name, id, grade 这三个属性
由于不存在一个类型同时满足 两个以上的基本类型,因此以下的类型都是 never
type T1 = string & number;
type T2 = string & bigint;
type T3 = string & symbol;
type T4 = number & bigint;
type T5 = number & symbol;
type T6 = bigint & symbol;
// ...
类型断言
告诉解析器变量的实际类型
as or <sometype>
// 类型断言,告诉解析器变量的实际类型
s = the_unknown as string;
s = <string> the_unknown;
类型别名
type 用一个别名指代某种类型
type myType = 1 | 2 | {name: string};
let l: myType;
interface 和 type
两者的相同点:
- 都可以定义 对象 或 函数
- 都允许继承
两者的不同点:
- interface 是 TS 用来定义对象的,type 是用来定义别名方便使用的
- type 可以定义基本类型,interface 不行
- interface 可以合并重复声明,type 不行
使用时机:
- 组合、交叉类型时用 type
- 涉及到类(extends、implement)用 interface
泛型
用于创建可重用的代码(不仅支持当前类型,还能够方便后续扩展)
<someType> 来表示泛型(临时占位,后续通过推导来判断类型),一般用 T 命名。
function print<T>(arg: T): T {
console.log(arg);
return arg;
}
print({name: "zhangsan"}); // 此时 T 为 object
print(233); // 此时 T 为 number
基础操作符
typeof
获取类型
interface Person {
name: string;
age: number;
}
const sem: Person = {name: "John", age: 18};
type Sem = typeof sem; // type Sem = Person
keyof
获取所有键
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
in
遍历枚举类型
type Keys = "a" | "b" | "c";
type Obj = {
[p in Keys]: any;
} // -> {a: any, b: any, c: any}
T[K]
索引访问
interface Person {
name: string;
age: number;
}
let type1: Person['name']; // string
let type2: Person['age']; // number
extends
泛型约束
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity({value: 3}); // 错误
loggingIdentity({length: 5}); // 正确
常用工具类型
Partial
将类型属性变为可选
type Partial<T> = {
[P in keyof T]?: T[P];
}
Required
将类型属性变为必选
type Required<T> = {
[P in keyof T]~?: T[P];
}
Readonly
将类型属性变为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
Pick<T, K extends keyof T>
从一个复合类型中,取出几个想要的类型的组合
type Pick<T, K extends keyof T> = {
[key in K]: T[key];
}
例如
// 原始类型
interface TState {
name: string;
age: number;
like: string[];
}
// 如果只想要 name 和 age,粗暴的方式是重新定义一个
interface TSingleState {
name: string;
age: number;
} // 但是这和 TState 并没有关联,也就是说,当 TState 改变时, TSingleState 并不会变化
// 正确的方式
interface TSingleState_2 extends Pick<TState, "name" | "age"> {};
Record<K, T>
构造一个对象类型, Keys 表示对象的属性键、Type 表示对象的属性值,用于将一种类型属性映射到另一种类型
可以理解为:将 K 的每一个值都定义为 T 类型
type Record<K extends keyof any, T> = {
[P in K]: T;
}
举例
假设一条数据的状态值(state)有三种:
type state = "created" | "submitted" | "removed";
现在需要创建一个状态值映射对象,这个对象的成员是每一个状态值,成员的值都是 string 类型。
// 手动写
interface StatesInterface {
created: string;
submitted: string;
removed: string;
}
export const states: StatesInterface = {
created: "01",
submitted: "02",
removed: "03"
}
// 使用 Record
export const state: Record<state, string> = {
created: "01",
submitted: "02",
removed: "03"
} // 不需要再手动写一个 interface 进行约束
TypeScript 配置
编译选项
自动编译文件
使用 -w 指令,监视文件的变化并自动重新编译
tsc xxx.ts -w
自动编译整个项目
在 tsconfig.json 中,进行配置
与其他 json 不同,可以写注释
存在该文件时,直接 tsc 可以编译该目录下所有的 ts 文件
使用 tsc -w 监视所有 ts 文件
配置选项
include
-
定义希望被编译所在的目录
-
默认值为:
["**/*"],任意目录下的任意文件 -
示例:
"include": ["./src/**/*", "./tests/**/*"]
exclude
- 定义需要排除在外的目录
- 默认值:
["node_modules", "bower_components", jspm_packages]
extends
-
定义被继承的配置文件
-
示例:
"extends": "./configs/base"表示自动包含 configs/base.json 中的所有配置信息
files
-
指定被编译文件的列表,只有需要编译的文件少时使用
-
示例:
"files": [ "core.ts", "sys.ts", "types.ts" ]
compilerOptions
对编译器进行复杂的配置,包含很多子选项
-
target
-
指定 ts 被编译为 es 的版本(默认值为ES3)
-
可选值:ES3, ES5, ES6/ES2015, ES7/ES2016, ES2017, ES2018, ES2019, ES2020, ESNext(最新版本)
-
示例:
"compilerOptions": { "target": "ES6" }
-
-
module
- 指定模块化规范
- 可选值:ES6/ES2015, commonjs 等
-
lib
- 用来指定项目中要使用的库(一般情况下不改)
-
outDir
-
指定编译后文件所在的目录
-
示例:
"outDir": "./dist",
-
-
outFile
-
将代码合并为一个文件(一般不用,因为可以用打包工具)
-
示例:
"outFile": "./dist/app.js" -
注意:会合并全局作用域中的所有代码,如果要合并模块化中的必须指定 module 为 system 或 amd
-
-
allowJs
- 允许指定 js 文件进行编译
- 默认值为 false
-
checkJs
- 是否检查 js 的代码符合语法规范
- 默认值为 false
-
removeComments
- 是否移除注释
- 默认值为 false
-
noEmit
- 不生成编译后的文件(但是还会检查语法)
- 默认值为 false
-
noEmitOnError
- 当有错误时,不生成编译后的文件
- 默认值为 false
-
alwaysStrict
- 是否设置编译后的文件使用严格模式
- 默认值为 false
- 引入模块时,自动使用严格模式
-
noImplicitAny
- 是否不允许隐式 any 类型
- 默认值为 false
-
noImplicitThis
-
是否不允许不明确类型的 this
-
默认值为 false
-
手动指定 this 类型
function fn(this: Window) { }
-
-
strictNullChecks
-
严格的检查可能的空值
-
默认值为 false
-
可以进行判断:
-
if (box1 !== null) { box1.addEventlistener('click', function () { console.log('hello'); }); } box1?.addEventListener('click', function () {});
-
-
-
strict
- 所有严格检查的总开关
- 默认值为 false,如果设置为 true,则自动开启 alwaysStrict,noImplicitAny,noImplicitThis,strictNullChecks 等配置项
- 建议开启,减少出错的机率
与 declare 相关的
declaration:设置可以自动生成*.d.ts声明文件declarationDir:设置生成的*.d.ts声明文件的目录declarationMap:设置生成*.d.ts.map文件(sourceMap)emitDeclarationOnly:不生成js文件,仅仅生成*.d.ts和*.d.ts.map
使用 Webpack 打包 ts 代码
初步配置
- 搭建 webpack 框架:
npm init -y - 安装依赖:
npm i -D webpack webpack-cli typescript ts-loader - 编写 webpack 配置文件
webpack.config.js
//webpack.config.js
const path = require('path');
// 配置信息
module.exports = {
// 入口文件
entry: "./src/index.js",
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, 'dist'),
// 打包后文件
filename: "bundle.js"
},
// 指定webpack打包时要使用的模块
module: {
// 指定 loader 加载的规则
rules: [
{
// test 指定规则生效的文件
test: /\.ts$/, // 指定以 ts 结尾的文件
// 使用的 loader
use: 'ts-loader',
// 要排除的文件
exclude: /node-modules/
}
]
}
}
- 编写 ts 配置文件
tsconfig.json
//tsconfig.json
{
"compilerOptions": {
"module": "ES2015",
"target": "ES2015",
"strict": true
}
}
- 添加执行命令
//package.json
{
"scripts": {
"build": "webpack"
}
}
此后可以使用 npm run build 进行打包
自动创建 html 文件
- 安装插件
npm i -D html-webpack-plugin
- 引入配置文件
//webpack.config.js
const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
// 配置 webpack 插件
plugins: [
new HTMLWebpackPlugin({
title: "自定义的 title"
}),
]
// ...
}
此后编译会自动生成 html 并引入 js
plugin 的配置对象还可以传入其他属性进行进一步配置,
如:template 使用 html 模板
Webpack 开发服务器
- 安装
npm i -D webpack-dev-server
- 添加一个命令
//package.json
"script": {
"start": "webpack serve --open chrom.exe"
}
可以实时更新,一旦文件及逆行修改,会自动重新编译
构建前清除 dist
- 安装
npm i -D clean-webpack-plugin
- 引入到 webpack.config.js
//webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// ...
// 配置 webpack 插件
plugins: [
new CleanWebpackPlugin(),
]
// ...
}
设置引用模块
如果不配置,只能引入 js 模块
//webpack.config.js
module.exports = {
// ...
resolve: {
extensions: ['.ts', '.js']
}
// ...
}
babel
将新语法转化为旧语法,将新技术让旧浏览器支持
- 安装
npm i -D @babel/core @babel/preset-env bable-loader core-js
- 引入
//webpack.config.js
const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
// 指定 loader 加载的规则
rules: [
{
// test 指定规则生效的文件
test: /\.ts$/, // 指定以 ts 结尾的文件
// 使用的 loader
use: [
{
// 指定加载器
loader: "babel-loader",
// 设置
options: {
// 设置预定义的环境
presets: [
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 兼容的目标浏览器
targets: {
"chrome": "88"
},
// 指定 corejs 的版本
"core-js": "3",
// 使用 corejs 的方式 - 按需加载
"useBuiltIns": "usage"
}
]
]
}
},
'ts-loader'
],
// 要排除的文件
exclude: /node-modules/
}
]
// ...
}
TypeScript 实战
声明文件
declare
声明后即可在全局使用
-
声明(全局)变量
wx.request({ // Error: 找不到 wx // ... }) // 这时候可以用 declare 来声明为全局变量,让 TypeScript 编译器能够识别 declare var wx: any; // var 指变量 // 我们常用的全局变量已经由 TS 自动完成了声明 // in lib.es5.d.ts declare var JSON: JSON; declare var Math: Math; declare var Object: ObjectConstructor; -
声明常量
declare let myObj1 = {}; declare const myObj2 = {}; -
声明(全局)函数、(全局)类型
declare function eval(x: string): any; declare function isNaN(number: number): boolean; declare type Person = { name: string; age: number; } -
声明模块
// 在编译 ts 文件时,如果你想导入一个 .css 之类的格式的文件,如果没有经过像下面这样的声明,会提示语法错误 declare module '*.css'; declare module '*.less'; declare module '*.png'; -
声明作用域
declare namespace API { interface ResponseList { code: number; } } // in index.vue const res = ref(<API.ResponseList>)({}) // 直接使用命名空间的接口,不需要引入
.d.ts
声明文件定义。
在 .d.ts 声明变量等之后,在其他地方可以不用 import 导入而直接使用(需要在 tsconfig.json 中 include 数组里面添加 .d.ts 文件)
例如:
// in tsconfig.json
{
"compilerOptions": {
// ...
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/typings/*.d.ts", // 使用通配符包含所有 .d.ts 文件
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
@types
第三方库 TS 类型包
tsconfig.json
定义 TS 的配置,详见上文 TypeScript 配置
实战举例
泛型约束后端接口类型
import axios from 'axios';
interface API {
'/book/detail': {
id: number,
},
'/book/comment': {
id: number,
comment: string
}
}
function request<T extends keyof API>(url: T, obj: API[T]) {
return axios.post(url, obj);
}
正确使用
request('/book/comment', {
id: 1,
comment: '非常棒!'
})
可以避免一些错误
- 路径错误
request('/book/test', { // API 中没有这个 key
id: 1,
comment: '非常棒'
})
- 参数错误
request('/book/detail', {
id: 1,
comment: '非常棒!' // 报错,/book/detail 中不包含 comment 这个 key
})