TypeScript
Hello TypeScript
Highlight Fragments
动态弱类型语言存在一些缺点:变量可以赋值为任意的类型、缺乏编译时的类型检查、类型信息不明确所导致的代码可读性较差等。
如需查看 TypeScript 的版本可以使用 tsc -v
或 npm view typescript version
命令。
tsconfig.json
文件用于配置 TypeScript 编译器的行为,其通常是在 TypeScript 项目的根目录中。
By invoking tsc with no input files, in which case the compiler searches for the
tsconfig.json
file starting in the current directory and continuing up the parent directory chain.
/* tsconfig.json */
{
"include": [ // include 指定哪些 ts 文件需要进行编译
"./src/**/*" // **任意目录 *任意文件
],
"exclude": [ // exclude 指定哪些文件不需要进行编译
"./src/exc/**/*" // 默认排除 ["node_modules", "bower_component", "jspm_packages"]
],
"extend": "./config/base", // 当前配置文件会自动包含 config 目录下 base.json 中的所有配置信息
"files": [ // 待编译的文件较少时可以使用 files 指定需要编译的内容 -> 同 include
"core.ts",
"sys.ts",
...
],
"compilerOptions": { // 在初始化 tsc 时会自动建立 compilerOptions 配置项 => compilerOptions 决定了编译器如何对 ts 文件进行编译
"target": "es5", // 指定 ts 文件被编译为的 ES 版本
"module": "commonjs", // 编译后的 js 需要使用的模块化规范
// "lib": [], // 指定项目中需要用到的库 - 一般情况使用默认就好
"outDir": "./dist", // 指定编译后文件所在目录
// "outFile": "./", // 将代码合并为一个文件 - 若模块化则必须是 AMD 或者 system => 初始化默认注释
"allowJs": true, // 是否对 JS 文件进行编译 - 默认为 false
"checkJs": true, // 检查 JS 代码是否符合规范 -> 通常与 allowJs 开关状态一致
"removeComments": true, // 是否移除注释 —— 编译时是否携带注释
"noEmit": false, // 不生成编译后的文件 => 不用编译功能而只检查语法
"noEmitOnError": false, // 当有错误时不生成编译后的文件
"strict": true, // 所有严格检查的总开关 -- 设置为 true 则所有严格模式全部打开
/* 语法检查 */
"alwaysStrict": false, // 编译后文件是否使用严格模式 => js 使用导入导出会默认开启严格模式所以不会显示 "use strict"
"noImplicitAny": true, // 不允许隐式的 any 类型
"noImplicitThis": true, // 不允许不明确类型的 this
"strictNullChecks": false // 严格检查空值
}
}
}
ts-node 可以在 Node 环境下直接运行 TypeScript 文件,而无需先将其显式地编译为 JavaScript。
运行时库 tslib 会被 TypeScript 编译器自动的引用,tslib 可以提供各种辅助函数和实用工具。举个例子,在 TypeScript 中使用 async/await
语法,编译器会将这段代码转换成基于 Promise 的异步操作。在转换过程中,编译器会插入一些辅助函数(__awaiter
函数)来处理这段较新版本的异步操作。
@types/node 是一个用于 Node.js 的 TypeScript 类型定义包,其中包含了关于 Node.js 运行时和核心模块的类型信息,可以为 TypeScript 项目提供更好的类型检查和编辑器支持。比如 TypeScript 编译器可以在安装的 @types/node 包中找到核心模块 fs 对应的类型定义,以提供更准确的类型推断和代码提示。
在 TypeScript 中,通常约定使用小写字母来表示基本类型、自定义接口,以及类型别名等。
当变量的声明和赋值操作同时进行时,类型推导可以根据赋值表达式的类型推断出变量的类型,从而省略类型声明。
function greeter(language: string, yearOfBirth: number): string { // 定义参数类型与返回值类型
return "Hello, " + language + " => " + yearOfBirth;
}
let language = "Typescript";
let yearOfBirth: number; yearOfBirth = 2012;
console.log(greeter(language,yearOfBirth)); // Hello, Typescript => 2012
在 TypeScript 中,strictNullChecks
编译选项控制着对于 null
和 undefined
的严格检查:
- 当设置为
false
时,null
和undefined
的使用会受到限制,需要进行额外的检查。 - 当设置为
true
时,null
和undefined
可以分配给任何类型的变量,不要求额外检查。
当变量赋值为 null
或 undefined
时,如果没有明确的类型注解,这个变量会被视作 any
类型。
除自身外,null
和 undefined
也可以赋值给 void
类型,表示没有返回值的函数或表达式。
const nullValue = null; // 变量 nullValue 隐式具有 any 类型 ts(7005)
const undefinedValue = undefined; // 变量 undefinedValue 隐式具有 any 类型 ts(7005)
let voidValue: void = null; // null 赋值给 void
voidValue = undefined; // undefined 赋值给 void
never
表示永远不会发生的类型,通常用于函数返回或永远不会被赋值的变量。
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// 执行无限循环,永远不会结束
}
}
// 变量被推断为 never 类型
const result: never = throwError("Something went wrong");
unknown
是一种需要进行类型检查或类型断言的安全类型,可以接收任何类型的值,但 unknown
类型的值只能赋给 unknown
本身和 any
类型。
let myVariable: unknown = "Hello";
let length: number = (myVariable as string).length; // 使用类型断言将 unknown 类型断言为 string 类型
console.log(length); // 输出字符串的长度
function processValue(value: unknown): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (typeof value === "number") {
console.log(value.toFixed(2));
} else {
console.log("Unknown value");
}
}
processValue("Hello"); // 可以处理字符串类型
processValue(10); // 可以处理数字类型
processValue(true); // 无法确定类型,需要进行额外处理
有些变量的值可能来自于动态的内容,比如用户输入或第三方代码库。在这种情况下,可能不希望类型检查器对这些值进行严格的类型检查,那么就可以使用 any
类型。
数组适合用于存储多个相同类型的元素,并且长度可以动态调整;而元组适合用于存储固定数量、不同类型的元素。当访问或修改超出元组长度范围的索引时,TypeScript 编译器会报错。
const arr01: string[] = ["ok", "okk"];
const arr02: Array<string> = ["ok", "okk"];
let tupleTypeTest01: [string, number]; // let tupleTypeTest01: Array<string | number>;
tupleTypeTest01 = ["hello", 6]; // 注意顺序
类型断言的作用是告诉编译器这个变量的实际类型比当前类型更确切,从而避免编译器对类型进行过于严格的检查。需要注意的是,类型断言并不会改变变量的实际类型,且只在编译阶段起作用,不会对运行时产生影响。由于类型断言会绕过类型检查机制,如果类型断言不正确,可能会导致运行时错误。
类型断言语法有尖括号和 as
两种形式。在使用 JSX 语法时,只能使用后者进行类型断言。
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
操作符 keyof
用于获取类型的属性名称组成的联合类型,而 typeof
用于获取值的类型信息。
type Name = { name: string }
type Age = { age: number }
type Union = Name | Age
type UnionKey<P> = P extends infer P ? keyof P : never
type T = UnionKey<Union> // T 的类型将会是 "name" | "age"
const num = 10;
type NumType = typeof num; // NumType 的类型为 number
关键字 infer
允许在泛型类型的条件判断中推断出一个类型,并将其赋值给一个新的类型变量。
// 使用 infer 关键字配合条件类型来实现 ReturnType 类型
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : T;
type MyFunction = () => number;
let Result: MyReturn<MyFunction>; // Result 类型为 number
type PromiseType<T> = T extends Promise<infer R> ? PromiseType<R> : T;
type pt = PromiseType<Promise<number>>; // type pt = number
type ptt = PromiseType<Promise<Promise<string>>>; // type ptt = string
type FirstArg<T> = T extends (first: infer R, ...args: any[]) => any ? R : T;
type fa = FirstArg<(name: string, age: number) => void>; // type fa = string
type ArrayItemType<T> = T extends (infer I)[] ? I : T;
type ItemType1 = ArrayItemType<[string, number]>; // type ItemType1 = string | number
type ItemType2 = ArrayItemType<string[]>; // type ItemType2 = string
Webpack Configs
初始化并安装构建工具,在根目录下创建 Webpack 的配置文件 webpack.config.js。
# 下载构建工具
npm i -D webpack webpack-cli typescript ts-loader # ts-loader => ts 加载器用于在 webpack 中编译 ts
npm i -D webpack-dev-server html-webpack-plugin clean-webpack-plugin # html-webpack-plugin => 用来自动创建 html 文件 \ clean-webpack-plugin => 每次构建都会先清除目录
/* webpack.config.js */
const path = require('path');
// 引入 html 插件 —— 自动生成 html 文件
const HTMLWebpackPlugin = require('html-webpack-plugin');
// 引入 clean 插件
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
// webpack中的所有的配置信息都应该写在 module.exports 中
module.exports = {
optimization:{
minimize: false // 关闭代码压缩 - 可选
},
// 指定入口文件
entry: "./src/index.ts",
devtool: "inline-source-map", // 生成一个 DataUrl 形式的 SourceMap 文件
devServer: {
contentBase: './dist',
open: {
app: { name: 'google-chrome', },
},
},
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, 'dist'),
// 打包后文件的文件
filename: "bundle.js",
// 默认打包后是一个立即执行的箭头函数 => 在 IE11 中无法执行
environment: { arrowFunction: false } // Webpack 打包时最外层不是箭头函数
},
resolve: { // 设置引用模块 —— 默认识别不出 .ts 结尾的模块
extensions: ['.ts', '.js'] // 尝试按顺序解析这些后缀名
}
// 指定 Webpack 打包时要使用的模块
module: {
rules: [ // 指定要加载的规则 => 从后向前加载
{
test: /\.ts$/, // 指定规则生效的文件
use: {
loader:'ts-loader', // ts 转换为 js
},
exclude: /node-modules/ // 要排除的文件
}
]
},
plugins: [
new CleanWebpackPlugin(), // 编译前自动清空 dist 目录
// html 文件的自定义配置 => 根据此 html 模板生成页面
// template 使用时就没必要加上 title => 以指定文件为模板生产 html
new HTMLWebpackPlugin({ title: "这是一个自定义的title", template: "./src/index.html" }),
],
}
根目录下创建 tsconfig.json,并修改 package.json 配置。
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"strict": true
}
}
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open-app-name 'google-chrome'"
}
}
在 Webpack 之外,开发中还经常需要 Babel 对代码进行转换以兼容到更多的浏览器,在此前配置的基础上,可通过以下新增配置将 Babel 引入项目。
安装 Babel 依赖,且在 webpack.config.js 中新增配置。
# @babel/core => 核心工具、@babel/preset-env => 预定义环境、@babel-loader => 在 Webpack 中的加载器、core-js => 使老版本的浏览器支持新版 ES 语法
npm i -D @babel/core @babel/preset-env babel-loader core-js
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: "babel-loader",
options:{
presets: [
[
"@babel/preset-env",
{
"targets":{ "chrome": "58", "ie": "11" },
"corejs":"3", // 模拟 js 运行环境代码 => 让旧版本浏览器使用新标准技术
"useBuiltIns": "usage" // corejs 按需加载
}
]
]
}
},
{
loader: "ts-loader",
}
],
exclude: /node_modules/
}
]
}
OOP For TS
TypeScript 在 JavaScript 的基础上添加了类型系统,故其也具有封装、继承、多态和抽象的特性:
- Encapsulation 封装:支持通过访问修饰符来控制类成员的访问权限
- Inheritance 继承:子类可以通过
extends
继承父类的属性和方法 - Polymorphism 多态:子类可以通过重写或者调用
super
关键字来扩展或修改父类的行为 - Abstraction 抽象:通过
abstract
关键字可以定义具有共同特征和行为的对象的基类
Class and Interface
实例属性即直接定义在类中的属性,需要通过对象实例才能访问和修改。静态属性可以直接通过类来访问,无需实例化对象。只读属性一旦赋值后就无法修改。
class Person {
readonly name: string = 'kakashi'; // 不可被修该 —— 只读属性
static age: number = 24;
}
const per = new Person();
console.log(Person.age, per.name); // 24 kakashi
在大多数情况下,一个类可能会创建出多个具有不同属性和方法的对象。因此,在定义类时并不需要为属性提供具体的值,而是在创建对象时为变量赋值。也就是说,可以通过构造函数在创建对象时进行属性的初始化。
class Personn {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name; // 通过 this 向新建对象添加属性
this.age = age;
console.log(this);
}
sayHello() {
console.log(`hello ${this.name} => ${this.age}`);
}
}
const p1 = new Personn("hello", 22);
p1.sayHello(); // hello hello => 22
编译器会检查不同文件中是否存在有变量名的重复,此时可以通过立即执行函数创建独立的作用域来进行规避。
不同的类中可能存在有相同的属性与方法,可以将这些重复的内容封装成超类,让需要这些数据的派生类进行继承。如果子类具有和父类中同名的方法,那么子类中的方法会重写父类方法。
由于名字相同的函数会发生重写,子类声明的构造函数会覆盖父类的构造函数。如果子类的构造函数没有调用父类的构造函数,那么父类的构造函数就不会被执行,这可能会导致部分属性的赋值失效。为确保继承关系不受影响,应该在子类的构造函数中通过 super(...args)
调用父类的构造函数。
(function () {
class Fruit {
name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
console.log("Okk");
}
}
class Coconut extends Fruit {
age: number;
constructor(name: string, age: number) {
super(name); // 调用父类的构造函数
this.age = age;
}
sayHello() {
super.sayHello(); // super 表示当前类的父类
console.log(`other methods => ${this.age} - ${this.name} - Fruit`); // other methods => 0.2 - coco - Fruit
}
}
const coco = new Coconut("coco", 0.2);
coco.sayHello(); // Okk —— 实际在调用父类的 sayHello
})();
抽象类是包含抽象方法的类,但是抽象类也可以没有抽象方法。抽象类不能被实例化,只能作为其他类的基类被继承。抽象方法是在抽象类中声明但没有具体实现的方法,只有方法签名而没有方法体。继承抽象类的子类必须实现(重写)抽象类中的抽象方法,否则子类也必须声明为抽象类。
(function () {
abstract class Hello { // 没有抽象方法的抽象类
sayHello(): void {
console.log("hello world");
}
}
class TShello extends Hello {}
let tshello = new TShello();
tshello.sayHello(); // hello world
})();
(function () {
abstract class Animal { // 无法创建抽象类的实例
name: string;
constructor(name: string) {
this.name = name;
}
abstract sayHello(): void; // 通用方法不能满足每个类的需求
}
class Cat extends Animal {
sayHello() {
// 必须重写抽象方法
console.log(`${this.name}` +" => cat class rewrite this method");
}
}
let cat = new Cat("hello kitty");
cat.sayHello(); // hello kitty => cat class rewrite this method
})();
接口是一种抽象的概念,用于描述对象的结构和行为。它定义了一组方法和属性的规范,但不提供具体的实现细节。接口可以被其他类或者对象实现,以确保遵循接口所定义的结构和行为。
接口中的方法都是隐式抽象的,这意味着接口只定义方法的签名而不包含具体的实现。在实现接口的类中,需要提供接口所定义的所有属性和方法,以满足接口的要求。
在 TypeScript 中,可以使用接口 interface
来为函数、数组和类(可索引类型)进行声明。
interface MyFunction {
(param1: string, param2: number): void; // 接受两个参数且函数本身没有返回值
}
const myFunc: MyFunction = (param1, param2) => { ... };
interface MyArray {
[index: number]: string; // 具有数字索引和字符串类型元素的数组类型
}
const myArr: MyArray = ["item1", "item2", "item3"];
interface MyClass { // 具有字符串索引和数字类型属性的类类型
[index: string]: number;
length: number;
}
const myObj: MyClass = {
prop1: 10,
prop2: 20,
length: 2,
};
在 TypeScript 中,通常用 interface
描述数据结构,用 type
描述类型关系:
- 拓展性:接口
interface
和类型别名type
都可以进行扩展(甚至互相拓展)。 - 声明合并:定义多个同名的
interface
会进行合并,而定义同名的type
会出现报错。 - 类型操作符:
type
支持更多的类型操作符,比如&
交叉类型和|
联合类型。
(function () {
// 定义对象类型限制类的结构 => 对象类型只定义结构不考虑实际值
type myType = { name: string; age: number };
const obj01: myType = {
name: "ok",
age: 22,
};
// 若定义多个同名接口则默认会将其不重复内容合并
interface mmyInterface {
name: string;
}
interface mmyInterface {
age: number;
}
const obj02: mmyInterface = {
name: "obj02",
age: 2,
};
// 定义接口限制类的结构 => 接口只定义结构不考虑实际值
interface myInterface1 {
name: string;
age: number;
}
interface myInterface2 {
sayHi(): void;
}
const obj03: myInterface1 & myInterface2 = {
name: "okk",
age: 23,
sayHi() {
console.log("implements myInterface1 & myInterface2"); // implements myInterface1 & myInterface2
},
};
obj03.sayHi();
// 接口的实现
class Obj04 implements myInterface1, myInterface2 {
name = "obj03";
age = 3;
sayHi() {
console.log("implements myInterface1 + myInterface2");
}
}
const obj04 = new Obj04();
obj03.sayHi(); // implements myInterface1 + myInterface2
})();
(function () {
interface myInterface {
name: string;
age: number;
sayHi(): void;
}
class MyClass implements myInterface { // 实现接口即使实现接口的所有属性与方法
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHi(): void {
// throw new Error("Guess method is not implemented.");
console.log(`${this.name}` + ' => ' + `${this.age}`)
}
name: string;
age: number;
}
const myclass = new MyClass("Test",123);
myclass.sayHi() // Test => 123
})();
TypeScript 中抽象类和接口的区别:
- 抽象类中的成员可以有访问修饰符,接口中的成员默认被
public
饰符 - 抽象类和接口都不能被实例化,前者可以被继承,后者可以被实现
- 一个类只能继承一个抽象类,但可以实现多个接口
- 抽象类可以有静态代码块和静态方法,接口中不能含有静态代码块以及静态方法
Modifiers and Generic
Member Visibility refers to the concept of controlling whether certain methods or properties are visible to code outside the class.
Visibility modifiers (access modifiers) includes public, private, protected and readonly.
默认的访问修饰符 public
表示该属性或方法可以在任意的位置被访问或修改。
被私有属性修饰符 private
修饰的属性或方法禁止被外部修改,只能在类的内部被修改。
属性修饰符 protected
表示该属性或方法只能在当前类或者当前类的子类中被访问和修改。
修饰符 readonly
可以声明类的属性或方法为只读成员,一旦标记则后续无法修改。
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
定义函数或类时,有些情况下无法确定其中要使用的具体类型(返回值、参数、属性的类型不能确定),此时考虑泛型。泛型只有在执行时才会确定。
// 泛型类
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
// 使用泛型类
let container = new Container<number>(42);
console.log(container.getValue()); // 输出: 42
// 泛型接口
interface Pair<T, U> {
first: T;
second: U;
}
// 使用泛型接口
let pair: Pair<string, number> = { first: "one", second: 1 };
console.log(pair); // 输出: { first: 'one', second: 1 }
Additional Supplement
可选链操作符 ?.
遇到 null
或 undefined
时会立即停止表达式的运行,并返回 undefined
。
非空断言运算符 !
用于断言一个表达式的值为非空,可以应用于变量、属性、函数返回值等。
interface User {
name?: string;
age?: number;
}
const user: User | null = null;
const userName = user?.name; // 当 user 为 null 时,userName 为 undefined
const userNameLength = user!.name!.length; // 断言 user 存在且 name 不为空,当 user 或 user.name 为 null 时,会导致运行时错误
声明文件 *.d.ts
用于提供 JavaScript 库的类型定义和声明。对于一些 JavaScript lib,可能会没有原生的 TypeScript 类型支持,但是社区会有提供相应的 @types/xxx
包用于补充(DT 标识)。
在 TypeScript 中,使用 import
语句导入省略扩展名的模块时,会按照以下顺序查找文件:
- 扩展名为
.ts
的 TypeScript 源代码文件。 - 扩展名为
.tsx
的 TypeScript 源代码文件(用于支持 JSX)。 - 扩展名为
.d.ts
的声明文件,用于提供 JavaScript 库的类型定义和声明。
const
关键字用于声明一个只读的常量,表示该变量的值不能被重新赋值。readonly
关键字用于定义只读属性或只读成员变量,可以应用于类的属性、接口的属性等。const
在运行时检查常量的值是否被修改,而 readonly
在编译时检查属性的可写性。
Bug Fix
- 编辑器提示函数重复实现或者无法重新声明块范围变量
TypeScript 编译器默认会将目录中的所有 TypeScript 文件在同一作用域下进行编译。
解决方案:创建独立的作用域或为每个文件生成单独的配置文件。
- .ts 文件引入其他 .ts 文件暴露的变量时出现报错
Webpack 默认不将 ts 作为模块使用,需进行配置通知哪些文件可作为模块使用。
/* webpack.config.js */
...
// 设置哪些文件可作为模块使用
resolve: {
extensions: ['.ts','.js']
}
...
- ts-node "xxx.ts" => zsh: command not found: ts-node => 缺失 ts-node
ts-node 是一个 TypeScript 执行引擎和 Node.js 的 REPL => 交互式解释器。
- TS7016 => 无法找到模块 "x" 的声明文件。"x" 隐式拥有 "any" 类型。
// 默认提示 => 若 "?" 包实际公开了此模块,可尝试添加包含 `declare module "?/.../x";` 的新声明 (.d.ts) 文件。
此问题通常在 TypeScript 项目里使用第三方的 JavaScript 库时产生。
由于引入文件里的变量类型不明确,需要 .d.ts 声明文件通知变量类型。
通常 TypeScript 会解析项目中所有的 *.ts 文件(包含以 .d.ts 结尾的文件)。
若 .d.ts 文件没有用到 import 或 export,那么最顶层声明的变量就是全局变量。
根目录创建 types 文件夹,并于此新建所需的模块类型声明文件;
tsconfig.json 中于 include 添加上 types。
- ts => 在配置文件 "/tsconfig.json "中找不到任何输入。指定的 "include" 路径为
["**/*"]
,"exclude" 路径为[]
。
tsconfig.json 存在后,VSC 会自动在 include 与 exclude 范围中查找 ts 文件,若找不到 ts 文件则报错。在 include 和 exclude 范围中添加 ts 文件可解决此问题。
结束
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!