typescript随笔
本文主要记录一些关于 typescript 的 API 使用方法和一些底层的原理。
供自己以后查漏补缺,也欢迎同道朋友交流学习。
介绍
TypeScript
是一种由微软开发的开源编程语言,它是 JavaScript
的一个超集,即 TypeScript 添加了额外的类型定义和一些其他功能,最终会被编译成普通的 JavaScript 代码。它旨在解决 JavaScript 在大型应用程序开发中遇到的不足,如缺少静态类型检查、接口、类以及模块等特性。由于其提供的额外安全性和开发效率提升,TypeScript 越来越受到开发者的欢迎。
特点
TypeScript 的主要特点包括:
- 静态类型检查:在编译阶段就能发现类型错误,有助于开发者提前发现并修正错误。
- 类和接口:支持面向对象编程,可以定义类、继承和接口,提高代码的复用性和结构化。
- 模块:通过导入和导出语句支持代码的模块化,有利于组织和管理大型项目。
- 泛型:允许创建可重用的组件,这些组件可以在不指定具体类型的前期下操作多种数据类型。
- 装饰器:提供了一种在类声明、方法、访问器、属性或参数上添加元数据或修改类的行为的方式。
安装和配置
安装TypeScript
-
前提条件:确保你的系统已安装 Node.js,因为 TypeScript 依赖于 Node.js 的npm包管理器进行安装。
-
全局安装TypeScript:打开终端或命令提示符,运行以下命令来全局安装 TypeScript:
## 使用npm全局安装
npm install -g typescript
## or 使用yarn全局安装
yarn global add typescript
## 验证TypeScript是否安装成功
tsc --version
创建TypeScript项目
## 初始化项目:在你希望创建项目的目录下,运行下面命令
npm init
## 安装依赖
npm install --save-dev typescript
配置TypeScript
## 在显目根目录下创建tsconfig.json
npx tsc --init
配置示例
在tsconfig.json
中,你可以设置比如目标 JavaScript 版本(target
)、模块系统(module
)、源码目录(include
)、排除目录(exclude
)等。一个基本的配置可能如下所示:
{
"compilerOptions": {
"target": "es6", // 或者 "es2020" 等,根据你的目标环境选择
"module": "commonjs", // 或 "esnext" 如果你使用现代模块系统
"strict": true, // 启用严格的类型检查
"esModuleInterop": true, // 使模块间交互更加友好
"outDir": "./dist", // 编译输出目录
"sourceMap": true // 生成源码映射文件,便于调试
},
"include": ["./src/**/*"], // 指定要编译的源文件目录
"exclude": ["node_modules"] // 排除不需要编译的目录
}
编译TypeScript
配置完成后,你可以通过运行以下命令来编译你的TypeScript源码:
npx tsc
这将根据 tsconfig.json
中的配置编译你的TypeScript文件,并将编译后的JavaScript文件输出到指定的目录。
基础
内置对象
TypeScript 继承了 ECMAScript
的所有内置对象。以下是一些主要的内置对象:
- Boolean:布尔值对象,用于封装一个布尔值。
- Error:错误对象,用于生成错误信息。
- Date:日期对象,处理日期和时间。
- RegExp:正则表达式对象,用于匹配文本模式。
- Number:数值对象,封装数字并提供相关操作方法。
- String:字符串对象,封装字符串并提供操作字符串的方法。
如果在浏览器环境中使用 TypeScript,还会有额外的针对 DOM 和 BOM 的内置对象:
- Document:代表整个 HTML 文档的对象。
- HTMLElement:基类,表示任何 HTML 元素。
- Event:事件对象,封装了发生的事件信息。
- NodeList:非实时集合,包含了按照指定选择器选取的一组节点。
在 TypeScript 中,你不需要特别引入这些内置对象,它们可以直接在代码中使用。例如,创建一个错误对象可以简单地写作:
const error = new Error();
原始数据类型
在 TypeScript 中,使用原始数据类型的基本方法如下:
// 布尔值(boolean)
const isDone: boolean = false;
// 数值(number)
const decimal: number = 6; // 十进制
const binary: number = 0b1010; // 二进制
const octal: number = 0o744; // 八进制
const hex: number = 0xf00d; // 十六进制
// 字符串(string)
const name: string = "niunai";
const fullName: string = `niunai, age is ${32}`;
// 空值(null)
const status: null | undefined = null;
// 未定义(undefined)
const u: undefined = undefined;
// 符号(Symbol)
const sym1: symbol = Symbol();
const sym2: symbol = Symbol("key");
// 大整数(BigInt)(用于表示大于`2^53 - 1`的整数)
const bigInt: bigint = 1020023339011239123n; // 注意结尾的'n'
任意值
在 TypeScript 中,any
类型是一个特殊类型,表示可以是任何类型。当你不清楚一个值的具体类型,或者希望跳过 TypeScript 的类型检查时,可以使用 any
类型。但是,过度使用 any
会削弱 TypeScript 的类型安全优势,因此应谨慎使用。
const anyValue: any = someFunction();
数组类型
在 TypeScript 中,数组类型可以明确指定每个元素的类型,以增强类型安全。有几种不同的方式来定义数组类型:
// 类型后缀表示法 最常用
const numberArray: number[] = [1, 2, 3];
const stringArray: string[] = ['a', 'b', 'c'];
// 泛型数组类型 Array<T>
const numberArray: Array<number> = [1, 2, 3];
const stringArray: Array<string> = ['a', 'b', 'c'];
// 元组类型 Tuple
const tuple: [string, number, boolean] = ['hello', 11, true];
// 只读数组 ReadonlyArray<T>
const readonlyArray: ReadonlyArray<number> = [1, 2, 3];
// readonlyArray.push(4); // 这会导致编译错误
// 类型断言和类型推断
const mixedArray = [1, 'two', true]; // 编译器会推断为 (number | string | boolean)[]
const explicitlyTypedArray: (number | string)[] = [1, 'two']; // 明确定义混合类型数组
函数类型
在 TypeScript 中,函数类型用于描述函数的输入(参数类型)和输出(返回值类型)。定义函数类型主要有两种方式:匿名函数类型和具名函数声明。
匿名函数类型的定义
匿名函数类型直接定义了函数参数和返回值的类型,不绑定到特定的函数名。这种类型通常用于类型注解或变量声明中。
// 函数类型定义
let add: (x: number, y: number) => number;
// 实现该类型的函数赋值给变量
add = function(x: number, y: number): number {
return x + y;
};
具名函数声明
具名函数声明不仅定义了函数的类型,还为函数赋予了一个名字,可以在代码中直接调用。
function add(x: number, y: number): number {
return x + y;
}
这里,add
函数被明确声明为接收两个number
参数并返回number
类型值的函数。
函数重载
TypeScript还支持函数重载,允许为同一个函数名提供多个类型签名,根据传入参数的不同自动匹配合适的实现。
function print(value: string): void;
function print(value: number): void;
function print(value: any): void {
console.log(value);
}
print('Hello'); // 正确
print(123); // 正确
对象类型 - 接口
接口是 TypeScript 中定义和强制执行对象结构的强大工具。它们不仅能够确保代码中对象的正确性,还能帮助文档化代码,提升开发者的理解速度和代码的可维护性。通过组合使用接口、类实现接口、接口扩展等特性,可以构建复杂而灵活的类型系统。
基本接口定义
interface Person {
firstName: string;
lastName: string;
age?: number; // 问号表示此属性是可选的
}
// 使用接口
let person: Person = {
firstName: "John",
lastName: "Doe",
};
扩展接口
接口还可以扩展其他接口,实现接口的复用和层次化定义:
interface Employee extends Person {
employeeId: number;
department: string;
}
// 使用扩展的接口
let employee: Employee = {
firstName: "Jane",
lastName: "Doe",
employeeId: 123,
department: "HR",
};
函数参数和返回值类型
接口也可以用来定义函数的参数类型或返回值类型:
interface GreetingFunc {
(name: string): string;
}
// 使用函数类型
let sayHello: GreetingFunc = function(name: string): string {
return `Hello, ${name}!`;
};
索引签名
接口可以使用索引签名定义可索引的对象,比如数组或字典类型:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray = ["Bob", "Alice"];
Type
在 TypeScript 中,type
是另一种创建新类型的手段,与 interface
有着相似之处,但也存在关键区别。 type
主要用于定义别名( aliases )、联合类型( Union Types )、元组类型( Tuple Types )、字面量类型( Literal Types )以及枚举类型( Enums,虽然通常直接使用 enum
关键字)。让我们逐一探讨这些用法:
类型别名(Type Aliases)
类型别名用于给已存在的类型起一个新的名字,增加代码的可读性。它可以用来替代任何类型,包括基本类型、接口、联合类型等。
type StringOrNumber = string | number;
function logValue(value: StringOrNumber) {
console.log(value);
}
logValue("Hello"); // 正确
logValue(123); // 正确
联合类型(Union Types)
联合类型表示一个值可以是几种类型之一。使用 type
定义联合类型可以更易于阅读。
type UserID = string | number;
function getUser(id: UserID) {
// ...
}
元组类型(Tuple Types)
元组类型允许表示一个已知元素数量和类型的数组。使用type
定义元组类型可以提供更清晰的意图。
type Coordinate = [number, number];
const point: Coordinate = [10, 20];
字面量类型(Literal Types)
字面量类型允许你指定一个具体的值作为类型,这对于精确控制变量的可能值很有用。
type Color = "red" | "green" | "blue";
function setBackgroundColor(color: Color) {
// ...
}
setBackgroundColor("red"); // 正确
setBackgroundColor("yellow"); // 错误
与接口的区别
- 接口
interface
则更侧重于描述对象结构,可以被实现(通过类),可以扩展,适合描述具有相同属性或方法的多个实体的公共结构。 - 类型别名
type
提供了更多关于类型定义的灵活性,可以用于联合类型、元组、字面量类型等,不支持实现或扩展,但可以被其他类型别名再次引用。
枚举
在 TypeScript 中,枚举( Enum
)是一种数值或字符串常量的集合,它为一组相关的值提供了友好的名字。枚举允许你以更可读的方式引用这些常量,而不是直接使用魔法数字或字符串,从而增加了代码的可维护性和自解释性。下面是一些基本的枚举用法:
// 基本枚举 默认情况下,第一个成员的值为0,之后每个成员依次递增1。
enum Color {Red, Green, Blue}
console.log(Color.Red); // 输出 0
console.log(Color[0]); // 输出 "Red"
// 指定成员值
enum Color {
Red = 8,
Green = 16,
Blue = 32,
}
console.log(Color.Red); // 输出 8
// 字符串枚举
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
console.log(Color.Red); // 输出 "RED"
元组
在 TypeScript 中,元组( Tuple )是一种特殊的数据类型,它允许你创建一个固定长度的数组,其中每个元素可以有不同的类型。元组为那些需要多种类型数据组合在一起但又保持各自类型的场景提供了便利。下面是一些关于元组的基本概念和使用方法:
// 基本定义: 元组的定义通过在方括号内列出类型来完成,各类型之间用逗号分隔。
let myTuple: [string, number, boolean];
// 初始化和访问
myTuple = ["Alice", 30, true];
console.log(myTuple[0]); // 输出 "Alice"
console.log(myTuple[1]); // 输出 30
// 解构赋值
let [name, age, isAdmin] = myTuple;
console.log(name); // 输出 "Alice"
console.log(age); // 输出 30
console.log(isAdmin); // 输出 true
// 可选元素与默认值
// 元组的元素也可以是可选的,通过在类型后加上`?`来表示
// 在解构时,未提供的可选元素会被赋予`undefined`
let optionalTuple: [string, number?, boolean] = ["Bob"];
// 元组的长度和灵活性
// 可以将一个元组与另一个元组相接,形成一个新的元组,这在构建灵活的数据结构时很有用。
let firstTuple: [string, number] = ["Tom", 25];
let secondTuple: [boolean] = [true];
let combinedTuple: [...firstTuple, ...secondTuple] = ["Tom", 25, true];
类型断言
类型断言(Type Assertion)是TypeScript中一种告诉编译器“我相信这个值是某种类型”的方式。它不会修改变量的实际类型,只是让编译器按照你所断言的类型去处理这个值。类型断言有两种语法形式:
// 尖括号语法 value as Type
let someValue = "this is a string";
let strLength: number = (<string>someValue).length; // 或者使用 `someValue as string`
// as语法 推荐的用法,因为它更易读且与JavaScript的语法更接近。
let strLength: number = (someValue as string).length;
声明文件
声明文件(.d.ts
文件)是 TypeScript 与非 TypeScript 代码交互的关键桥梁,它们使得 TypeScript 能够为 JavaScript 代码提供类型检查和智能提示,提升开发体验和代码质量。正确管理和使用声明文件是 TypeScript 项目开发中的重要一环。
声明文件的用途
-
提供类型信息:对于没有类型注释的 JavaScript 库,声明文件提供必要的类型信息,让 TypeScript 知道函数、对象、模块的形状(即它们的属性和方法)。
-
兼容性:允许 TypeScript 项目无痛地引用和使用 JavaScript 库,保持代码的强类型检查和智能提示。
-
定义全局变量和模块:声明文件可以用来定义全局变量(如
window
上的挂载点)或声明外部模块的存在。
如何创建和使用声明文件
创建自定义声明文件:
如果你需要为某个未提供类型定义的库创建声明文件,可以创建一个 .d.ts
文件,并在其中定义该库的类型。例如,创建 myLib.d.ts
,并在其中声明一个函数类型:
declare function myFunction(name: string): void;
export default myFunction;
然后,在使用该库的 TypeScript 文件中,通过 import
语句引用它,并且 TypeScript 编译器就能识别出该函数的类型了。
使用第三方声明文件:
对于许多流行的 JavaScript 库,社区已经维护了对应的 .d.ts
文件,这些文件通常通过 @types
命名空间在 npm 上发布。你可以通过 npm
安装这些类型定义,例如:
npm install @types/node --save-dev
安装后,TypeScript 会自动找到这些类型定义,无需额外配置即可使用。
声明全局变量:
如果你的 JavaScript 代码中使用了全局变量,可以在声明文件中使用 declare
关键字来声明它们。例如,声明一个全局的 jQuery
变量:
declare var jQuery: any;
这样,即使你的项目中没有直接引入 jQuery 的类型定义,TypeScript 也不会报错。
模块声明:
如果你的库是一个模块,可以在声明文件中使用 declare module
来描述模块的导出接口:
declare module 'myModule' {
export function doSomething(): string;
}
类(Class)
类是面向对象编程的基础,用于创建具有属性(数据成员)和方法(成员函数)的对象的蓝图。TypeScript 中的类支持继承、封装、多态等面向对象特性。
基本语法
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
const person = new Person("Alice", 30);
person.greet(); // 输出 "Hello, my name is Alice and I am 30 years old."
继承
class Student extends Person {
studentId: string;
constructor(name: string, age: number, studentId: string) {
super(name, age);
this.studentId = studentId;
}
study() {
console.log(`${this.name} is studying.`);
}
}
const student = new Student("Bob", 20, "S123");
student.greet(); // 继承自Person类的方法
student.study();
模块(Module)
模块是用于组织代码的容器,它允许你将相关联的类、接口、函数等封装在一个单独的文件中,并可以控制它们的可见性(导出/导入)。模块有助于避免命名冲突和促进代码的复用。
导入与导出
// 导入(Import): 用于在其他文件中使用导出的元素
import { MyClass } from './moduleA';
const myInstance = new MyClass();
// 导出(Export): 用于使模块内的元素对外部可见
export class MyClass {
// ...
}
命名空间(Namespace)与模块的异同
在早期版本的 TypeScript 中,命名空间( Namespace
)是另一种组织代码的方式,它类似于 C# 或 Java 中的包,提供了一种分层次的方式来组织代码。虽然模块现在是推荐的做法,但命名空间仍然可用,特别是在需要合并多个文件定义的命名空间时。
高级类型探索
泛型
泛型( Generics
)是 TypeScript 中一个强大的特性,它允许你在定义函数、接口或类的时候不预先指定具体的类型,而是将类型作为参数传递。这样,你可以创建可重用的组件,这些组件可以在多种数据类型上工作,同时仍然保持类型安全。
基本概念
泛型的核心在于使用类型变量(通常用大写字母表示,如T
、U
等)来代表一些未知的类型。当使用这个组件时,你再指定这些类型变量的具体类型。
泛型函数
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("hello"); // T被指定为string类型
console.log(output); // 输出 "hello"
let numberOutput = identity<number>(123); // T被指定为number类型
console.log(numberOutput); // 输出 123
在这个例子中,identity
函数就是一个泛型函数,它接受一个类型参数 T
,并且返回值也是 T
类型。这意味着你可以用任何类型调用这个函数,而 TypeScript 会保证类型的一致性。
泛型接口
interface Pair<T> {
first: T;
second: T;
}
let pairStr: Pair<string> = { first: "hello", second: "world" };
let pairNum: Pair<number> = { first: 1, second: 2 };
泛型类
class Box<T> {
private containedValue: T;
set(value: T) {
this.containedValue = value;
}
get(): T {
return this.containedValue;
}
}
let boxStr = new Box<string>();
boxStr.set("hello");
console.log(boxStr.get()); // 输出 "hello"
let boxNum = new Box<number>();
boxNum.set(123);
console.log(boxNum.get()); // 输出 123
泛型约束
有时候,你可能需要限制可以作为类型参数的具体类型,这时候可以使用泛型约束。泛型约束通过接口来定义,要求传入的类型必须满足该接口定义的条件。泛型约束让你能够在编写泛型代码时,对类型参数施加限制,从而在编译时就能捕获潜在的错误。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 确保T类型有length属性
return arg;
}
loggingIdentity({length: 10, value: 'test'}); // 正确
// loggingIdentity(123); // 错误,因为number没有length属性
联合类型 Union Types
联合类型允许一个变量可能是多种类型之一。例如,你可以定义一个变量既可能是字符串也可能是数字:
let myValue: string | number;
myValue = "Hello";
myValue = 42;
类型守卫(Type Guards)
当你在操作联合类型的变量时,TypeScript 可能无法确定变量的具体类型,这会影响到你能够调用的方法或访问的属性。类型守卫就是用来缩小类型范围,确保在运行时变量属于某种特定类型。
类型守卫的方式
typeof 类型守卫:
用于检查原始类型(如 string
、 number
、 boolean
等)。
if (typeof myValue === "string") {
console.log(myValue.toUpperCase()); // 在这个分支里,TypeScript知道myValue是字符串
} else {
console.log(myValue.toFixed(2)); // 在这里,TypeScript知道myValue是数字
}
instanceof 类型守卫:
用于检查对象是否是某个构造函数的实例。
class Animal {}
class Dog extends Animal {
bark() {
console.log('Woof!');
}
}
function isDog(animal: Animal): animal is Dog {
return animal instanceof Dog;
}
let pet = new Dog();
if (isDog(pet)) {
pet.bark(); // 在这个分支,pet被断言为Dog类型
}
in 操作符:
检查对象是否有特定的属性。
interface Cat {
meow: () => void;
}
function makeSound(animal: Animal | Cat) {
if ('meow' in animal) {
animal.meow(); // animal在此被推断为Cat类型
} else {
console.log(animal.toString());
}
}
类型谓词(value is Type
):
类型谓词通常在函数中使用,通过返回一个布尔值并利用特殊的语法来通知TypeScript编译器该值的类型约束。
function isType<T>(value: T): value is Type {
// 实现逻辑判断value是否为Type
}
ts在react中的使用
新项目使用 create-react-app
接入
npx create-react-app my-app --template typescript
React老项目接入
首先安装 @types/react
和 @types/react-dom
这些React的类型定义文件:
npm install --save-dev @types/react @types/react-dom
然后将 .js
文件逐步转换为 .tsx
(TypeScript 支持 JSX 的文件扩展名)并添加类型注释。
React代码编写
import React, { useState } from 'react';
interface Props {
name: string;
}
const Hello: React.FC<Props> = ({ name }) => {
const [message, setMessage] = useState<string>('Hello');
return (
<div>
<h1>{`${message}, ${name}!`}</h1>
<button onClick={() => setMessage('Welcome')}>Change Message</button>
</div>
);
};
export default Hello;
ts在vue3中的使用
新项目使用 Vue CLI
接入
vue create my-vue3-project --preset typescript
Vue老项目接入
vue add typescript
Vue代码编写
<script lang="ts">
import { defineComponent, ref, reactive } from 'vue';
interface Props {
msg: string;
}
export default defineComponent({
props: {
msg: String
},
setup(props: Props) {
const count = ref(0);
const state = reactive({ status: 'active' });
// ...
}
});
</script>