阅读视图
前端新玩具:Vike 发布!
AI时代的前端知识拾遗:前端事件循环机制详解(基于 WHATWG 最新规范)
JavaScript 学习笔记:深入理解 map() 方法与面向对象特性
数据字典:从"猜谜游戏"到"优雅编程"的奇幻之旅
MJML邮件如何随宽度变化动态切换有几列📮
深入剖析 JavaScript 中 map() 与 parseInt 的“经典组合陷阱”
TypeScript核心类型系统完全指南
第一部分:TypeScript基础入门
TypeScript简介
1.什么是TypeScript
-
TS是JS的超集,简单来说就是为 js添加了类型限定。众所周知js的类型系统存在 先天的缺陷,程序中很多的问题都是因为错误的 类型导致的。ts属于静态类型编程语言,js属于动态编程语言
2. Ts的优势
- ts是前端项目的首选语言,ts中存在类型推断机制 不需要在代码中的每个地方都显示标注
体验TS与配置
体验ts
-
ts交于js会对数据类型进行检查
-
只需要第一次 定义变量的时候对 数据类型进行注解
let age:number=18; //:number 是类型注解 表示age变量的类型是number
常见的TS类型注解
-
原始类型
numberstringbooleansymbolnullundefined
-
对象类型
-
object(数组 对象 函数) -
联合类型
- 自定义类型(类型别名)
- 接口
- 元组
- 字面量类型
- 枚举
voidany
配置tsconfig.json
-
需要在根目录中被指ts的配置文件,这是ts开发必备的 操作之一
-
{ "compilerOptions": { "target": "ES2020", "module": "CommonJS", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true } }
-
第二部分:TypeScript核心类型系统
基础类型
原始类型
原始类型就是常见的 字符串 数字 布尔值 空 和 未定义 等等类型 基本上js怎么使用那么ts就这么使用 只是需要添加一些类型注解而已
let age: number=18;
let name: string='张三';
let sex: boolean=true;
let symbol: symbol=Symbol('123');
let nullValue: null=null;
let undefinedValue: undefined=undefined;
//js 里面怎么用 ts就是怎么用
数组类型
数组类型属于对象类型,在对象类型中每个子类型都有自己的细分语法
数组类型的类型注解: number[](推荐使用) 或者 :Array<string> 如果数组中存在多种数据类型就使用联合类型 (number | string)[]
//推荐写法
let numbers: number[]=[1,2,3,4,5];
let objArr:Object[]=[{},{}]
//其他写法
let strings:Array<string>=['1','2','3'];
//数组中含有多种类型数据 --> 联合类型
let arr: (number | string)[]=[1,'2',3,'4',5];
元组类型
元组可以看作确定元素个数与类型的数组,在部分场景里面会 使用到确定元素个数的数组类型 这种类型就叫做元组类型(比如:地图的经纬度)
元组类型类型注解::[number,number]
let Position=[31.232,12.653];
//元组的类型也不一定必须要一样
let Position2:[number,string]=[31.232,'12.653'];
枚举类型
枚举类型可以作为字面量类型的平替方案 枚举类型类似于字面量类型+联合类型的组合形态
枚举的值称为命名常量
定义枚举:
通过enum关键词定义 使用{}包裹命名常量
enum Direction{up,down,right,left}
枚举类型使用:
当使用枚举类型的函数需要调用的时候 只能通过枚举命名常量的属性来作为函数的参数
// 方向枚举
enum Direction {
Up,
Down,
Left,
Right
}
// 状态枚举
enum Status {
Pending = "PENDING",
Approved = "APPROVED",
Rejected = "REJECTED"
}
// 函数参数使用枚举
function move(direction: Direction) {
switch (direction) {
case Direction.Up:
console.log("向上移动");
break;
case Direction.Down:
console.log("向下移动");
break;
// ...其他情况
}
}
// 调用函数时使用枚举
move(Direction.Up);
枚举的种类
1.数字枚举
数字枚举是最常见的枚举类型,默认情况下第一个成员的值为0,后续成员按顺序递增:
enum Direction {
Up, // 值为 0
Down, // 值为 1
Left, // 值为 2
Right // 值为 3
}
// 使用枚举
let dir: Direction = Direction.Up; // 值为 0
console.log(dir); // 输出: 0
可以手动设置枚举成员的值:
// 设置起始值
enum Direction {
Up = 1, // 值为 1
Down, // 值为 2
Left, // 值为 3
Right // 值为 4
}
// 为每个成员设置具体值
enum Direction {
Up = 1,
Down = 3,
Left = 5,
Right = 9
}
2.字符枚举:
字符串枚举的每个成员都必须显式地初始化为字符串字面量,它们没有自增长行为:
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" }
// 使用字符串枚举
let dir: Direction = Direction.Up;
console.log(dir); // 输出: "UP"
- 异构枚举
虽然不建议使用,但技术上是可以混合字符串和数字成员的:
typescript
enum BooleanLikeHeterogeneousEnum { No = 0, Yes = "YES", }
字面量类型
字面量类型是一种特殊的类型,它将变量的类型限制为特定的值。与普通的string、number、boolean类型不同,字面量类型不仅指定了值的类型,还指定了值的具体内容。
字面量类型的特点:
- 精确性:字面量类型比普通类型更加精确,限定了变量只能是特定的值
-
常量推断:使用
const声明的变量,TypeScript会自动推断为字面量类型 -
可组合性:通过联合类型(
Union Types)可以组合多个字面量类型
字面量类型用法:
-
let str1='hello ts' //string类型 const str2='hello ts' //hello ts 字面量类型 -
字面量类型通常与联合类型结合使用,用于限制函数参数或配置对象的取值范围
// 限制函数参数的取值 function setPosition(direction: 'left' | 'right' | 'up' | 'down') { // 函数体 } // 调用时只能传入指定的字符串字面量 setPosition('left'); // 正确 setPosition('forward'); // 错误 // 配置对象的属性限制 interface Config { theme: 'light' | 'dark'; size: 'small' | 'medium' | 'large'; } const config: Config = { theme: 'dark', // 只能是'light'或'dark' size: 'medium' // 只能是'small'、'medium'或'large' };
与枚举类型相比
通常字面量类型与枚举类型可以替换
// 使用字面量类型
type Direction = 'up' | 'down' | 'left' | 'right';
// 使用枚举类型
enum DirectionEnum {
Up,
Down,
Left,
Right
}
联合类型
联合类型(Union Types)是TypeScript中的一种高级类型特性,它允许一个变量或参数可以是多种类型中的一种。联合类型使用竖线(|)分隔每个类型,表示"或"的关系。 后续在高级类型特性里面会详细介绍联合类型的各种使用方法
基本语法:
// 基本语法:Type1 | Type2 | Type3
let value: number | string;
value = 123; // 正确,number类型
value = "hello"; // 正确,string类型
value = true; // 错误,boolean类型不在联合类型中
复杂类型
对象类型
对象类型就是在描述对象的结构与各个属性的类型与 方法类似
1.基本写法
let person:
{
name:string;
age:number;
sayHi(name:string,age:number):void
} = {
name:'张三',
age:18,
sayHi(name:string):void{
console.log('hi',name)
}
}
2.箭头函数写法 :
//箭头函数写法
let person2:{
name:string;
age:number;
// 箭头的后面写返回值类型
sayHi:(name:string)=>void
} = {
name:'张三',
age:18,
sayHi:(name:string):void=>{
console.log('hi',name)
}
}
3.对象类型的可选属性
//对象类型可选属性
let person3:{
name:string;
age:number;
// 箭头的后面写返回值类型
sayHi:(name:string)=>void;
sex?:string;
} = {
name:'张三',
age:18,
sayHi:(name:string):void=>{
console.log('hi',name)
},
//sex:'man' //可有可无
}
函数类型
函数类型就是在js的基础上单独为行数的 参数 与返回值类型进行类型标注.
单独标注参数类型与返回值类型
就是单对为函数的参数与返回值进行类型标注
function add(X:number,Y:number):number{
return X+Y;
}
同时标注二者类型
这是第一种写法 可读性很差 前面两个类型定义是定义两个参数的 后面一个类型定义是定义返回值类型的
const add2:(num1:number,num2:number)=>number=(num1,num2)=>{
return num1+num2;
}
下面是可读性更高的一中 写法
// 定义函数类型别名
type AddFunction = (num1:number, num2:number) => number;
// 使用类型别名
const add2:AddFunction = (num1, num2) => {
return num1 + num2;
}
箭头函数常用的定义方法
const add1 = (X:number, Y:number):number => {
return X + Y;
}
返回值为void
function add3(X:number,Y:number):void{
console.log(X+Y);
}
可选参数类型
在参数后加一个? 就是可选参数 但是不建议使用可选参数在需要计算的函数中. 值得注意的是 必选参数一定要放在可选参数的前面
function add4(X:number,Y?:number):void{
console.log(X+(Y??1));
}
//必选参数不能位于可选参数之后
function mySlice(start?:number,end?:number):void{
console.log('开始',start,'结束',end);
}
mySlice()
特殊类型
any类型
any类型是我们极不推荐使用的类型 因为any类型不会对代码进行保护 和js基本没有两样了. 如果每个变量都说用any类型,那代码就和js基本一模一样了,失去了ts作为静态类型语言的作用了
let a:any = 123;
a = '123';
//any类型会忽略类型检查 还不如不用ts
void类型
void类型表示没有任何类型,通常用于函数没有返回值的情况。如果变量被注解为void类型一般只能赋值为null或者是undefined
基本用法
// 函数没有返回值时,返回类型标记为void
function sayHello(): void {
console.log("Hello!");
// 不需要return语句,或者可以return;
}
// 等同于
function sayHello2(): void {
console.log("Hello!");
return; // 可以显式返回undefined
}
// 变量声明为void类型(不常用)
let unusable: void = undefined; // void类型只能赋值为undefined或null
实际应用场景
// 事件处理函数通常没有返回值
function handleClick(event: Event): void {
console.log("按钮被点击了");
}
// 日志记录函数
function logMessage(message: string): void {
console.log(`[LOG]: ${message}`);
}
null和undefined
在TypeScript中,null和undefined都有各自的类型,分别是null和undefined类型。
基本用法
// null类型
let nullValue: null = null;
// undefined类型
let undefinedValue: undefined = undefined;
// 在严格模式下,null和undefined只能赋值给any类型和它们各自类型
let num: number = null; // 错误:在严格模式下不允许
let str: string = undefined; // 错误:在严格模式下不允许
与联合类型结合使用
// 变量可以是字符串或null
let userName: string | null = null;
userName = "张三"; // 正确
// 变量可以是数字或undefined
let userAge: number | undefined = undefined;
userAge = 25; // 正确
// 函数返回值可能是对象或null
function findUser(id: number): User | null {
// 查找用户逻辑
// 如果找到返回User对象,否则返回null
return null;
}
在React中的应用
// React中常见的状态初始化为null
const [user, setUser] = useState<User | null>(null);
// 使用可选链操作符安全访问属性
console.log(user?.name); // 如果user为null,不会报错
never类型
never类型表示永远不会发生的值的类型。它是TypeScript类型系统中的底部类型
使用场景
// 1. 函数抛出异常,永远不会有返回值
function throwError(message: string): never {
throw new Error(message);
}
// 2. 函数中有无限循环,永远不会结束
function infiniteLoop(): never {
while (true) {
// 无限循环
}
}
// 3. 类型守卫中的never
function exhaustiveCheck(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
类型特点
-
底部类型:
never是TypeScript类型系统中的底部类型,它是所有类型的子类型 -
不可赋值:除了
never本身,没有其他值可以赋值给never类型 -
类型推断:在某些情况下,
TypeScript会自动推断出never类型
第三部分:类型高级特性
类型别名(Type Alias)
类型别名 即为自定义类型 当统一类型被多次使用时,可以通过类型别名 简化该类型的使用
定义与使用
使用type关键字来创建类型别名
// 使用type关节子创建类型别名
type myType=(number|string)[]
let arr1:myType=[1,2,'3'];
console.log(arr1);
接口(Interface)
一般情况下如果一个对象类型被多次使用的时候,为了达到复用的目的,会使用接口来描述对象的类型
定义:
interface Person{
name:string;
age:number;
sayHi():void;
}
let person1:Person={
name:'张三',
age:18,
sayHi(){
console.log('hi',this.name)
}
}
接口的继承
接口可以使用extends来继承另一个接口中的类型注解
//如果两个接口有公共属性 就可以通过继承的方式实现复用
interface People{
name:string;
}
interface Teacther extends People{
age:number;
subject:string;
}
let t: Teacther={
name:'张三',
age:18,
subject:'Math'
}
接口的合并
// 接口声明合并
interface Window {
title: string;
}
interface Window {
ts: TypeScriptAPI;
}
// 现在Window接口有title和ts两个属性
接口与类型别名:
接口和类型别名很相似,都可以为对象指定类型. 但是区别也是很明显的
- 接口可以通过继承来拓展自身的类型注解
- 接口可以通过合并拓展自身的类型注解
- 类型别名可以为任何类型创建别名,但是接口只适用于对象
//结构与类型别名都可以为对象指定类型
type APerosn={
name:string,
age:number
}
interface BPerson{
name:string,
age:number
}
let a:APerosn={
name:'张三',
age:18
}
let b:BPerson={
name:'张三',
age:18
}
类(Class)
1.类的定义
TS中也引入了class的语法糖,写法基本和js中的class语法糖相似 不同的是 需要提前定义类中属性与方法的类型注解
//ts引入了class语法
class Person{
name:string;
age:number;
//构造函数就是一种方法 所以参数必须规定类型
constructor(name:string,age:number){
this.name = name;
this.age = age;
}
sayHi(){
//因为没有return TS自动做了类型推断 所以这个方法可以不用写void返回类型
console.log(`大家好,我叫${this.name},今年${this.age}岁`);
}
//类的实例方法和对象的方法是一样的 也需要指定类型
changeName(name:string){
this.name=name;
}
}
// 如果类没有类型 会自动默认为any属性
let p1=new Person('张三',18)
2.类的继承
类的继承分为两种 一种是继承父类 另一种是继承接口中的类型定义
类的继承和其他语言面向对象类似,可以重写继承来的方法 也可以省略不写 使用父类继承来的方法. 而接口的继承和jva`是类似的.
//ts有两种继承方法 1.extends(继承父类) 2.implements(实现接口 ts特有)
class Animal{
move(){
console.log('move');
}
}
class Dog extends Animal{
bark(){
console.log('bark');
}
}
let dog=new Dog();
dog.bark()
//2.implements 继承interface接口
//和java一样 接口只能定义属性和抽象方法(没有实现的方法)
interface Person{
name:string;
age:number;
move():void
}
class Student implements Person{
name: string;
age: number;
constructor(name:string,age:number){
this.name=name;
this.age=age;
}
move(){
console.log('move');
}
}
3.类成员的可见性
类中的成员属性有四种访问修饰符 不仅仅是修饰属性的 也可以修饰方法
-
public- 公开的,任何人都可以访问(默认) -
private- 私有的,只能在类内部访问 -
protected- 受保护的,只能在类和子类中访问 -
readonly- 只读的,只能在声明时或构造函数中初始化
演示代码
// 基类 - 演示所有四种访问修饰符
class Person {
// 1. public - 公开的,任何人都可以访问(默认)
public name: string;
// 2. private - 私有的,只能在类内部访问
private secret: string;
// 3. protected - 受保护的,只能在类和子类中访问
protected age: number;
// 4. readonly - 只读的,只能在声明时或构造函数中初始化
readonly id: number;
constructor(name: string, secret: string, age: number, id: number) {
this.name = name;
this.secret = secret;
this.age = age;
this.id = id;
}
// 公共方法可以访问所有成员
public introduce(): void {
console.log(`我叫${this.name},年龄${this.age},ID: ${this.id}`);
// console.log(this.secret); // 可以在类内部访问private成员
}
// 私有方法只能在类内部调用
private tellSecret(): void {
console.log(`我的秘密是: ${this.secret}`);
}
}
// 子类 - 继承Person类
class Student extends Person {
public grade: string;
constructor(name: string, secret: string, age: number, id: number, grade: string) {
super(name, secret, age, id);
this.grade = grade;
}
public study(): void {
console.log(`${this.name}正在学习`);
// 可以访问protected成员
console.log(`年龄: ${this.age}`);
// 不能访问private成员 - 会报错
// console.log(this.secret); // Error: Property 'secret' is private
// 可以访问public成员
console.log(`ID: ${this.id}`);
}
}
// 测试代码
const person = new Person('张三', '我喜欢吃糖', 25, 1001);
const student = new Student('李四', '我害怕考试', 18, 1002, '高三');
// 1. public成员 - 可以任意访问
console.log(person.name); // 输出: 张三
console.log(student.name); // 输出: 李四
// 2. private成员 - 不能在类外部访问
console.log(person.secret); // Error: Property 'secret' is private
console.log(student.secret); // Error: Property 'secret' is private
// 3. protected成员 - 不能在类外部访问
console.log(person.age); // Error: Property 'age' is protected
console.log(student.age); // Error: Property 'age' is protected
// 4. readonly成员 - 可以读取但不能修改
console.log(person.id); // 输出: 1001
// person.id = 1003; // Error: Cannot assign to 'id' because it is a read-only property
// 调用方法
person.introduce(); // 输出: 我叫张三,年龄25,ID: 1001
student.introduce(); // 输出: 我叫李四,年龄18,ID: 1002
student.study(); // 输出: 李四正在学习\n年龄: 18\nID: 1002
// 尝试修改readonly属性 - 编译时会报错
student.id = 1005;
// Error: Cannot assign to 'id' because it is a read-only property
泛型(Generics)
泛型基础概念
钻石运算符 <> 里面添加的是类型变量比如T 这个T是一个变量 往里填哪个类型 T就是什么类型 他是一个类型的容器 可以自动捕获用户提供的类型.
// 泛型是可以在保证安全的清况等下 让函数与多种类型一起工作 从而实现复用 常用于函数 接口 类中
// <>叫做钻石运算符 里面添加类型变量 比如T等等
//T是一个特殊的变量 他的处理类型不是值 他是一个类型的容器 可以自动捕获用户提供的类型
function id<T>(value:T):T{
return value;
}
const getId=<t>(Value:t):t=>{
return Value;
}
//调用
const num=<string>getId('123')
const num1=<number>getId(123)
//简化调用 调用泛型函数的时候 可以把尖括号省了 ts会自动识别类型(类型参数推断)
const num2=getId(123);
//有时候推断的类型可能不准确 就需要手动去定义
泛型约束
默认情况下 泛型函数的类型数量type可以代表多个类型 这导致无法访问任何属性 比如id('a')调用函数时参数的长度
function id<T>(value:T):T{
console.log(value.length);
return value
}
使用上面的函数会报错 因为T可以代表任意类型 无法保证一定存在length属性 此时就需要为泛型添加约束来收缩类型
有两种为泛型添加约束的方法
-
方法1: 为type指定更具体的类型
function id<T>(value:T[]):T[]{ console.log(value.length); return value } -
方法2: 定义接口为T添加约束
interface LengthWise{ length:number } function id<T extends LengthWise>(value:T):T{ console.log(value.length); return value }- 方法二的解释:
- 1.创建接口提供需要的属性 比如length
- 2.通过extends关键字使用该接口 为泛型(控制变量)添加约束
- 表述为 传入的类型必须具有length属性
- 方法二的解释:
多个泛型相互约束:
泛型的类型变量可以存在多个 而且类型变量之间也可以约束的 (比如 第二个类型变量受第一个变量的约束) 比如创建一个函数来获取兑现中属性的值
//泛型的类型变量可以存在多个 而且类型变量之间也可以约束的 (比如 第二个类型变量受第一个变量的约束) 比如创建一个函数来获取兑现中属性的值
function getProp<T,K extends keyof T>(Obj:T,key:K):T[K]{
return Obj[key];
}
let person={name:'jack',age:18}
console.log(getProp(person,'name')); //jack
//keyof关键字会接受一个对象类型 生成其键名称(可能是字符串或是数字)的联合类型
//实例中keyof T实际上获取的是person对象所有键的联合类型 也就是'name'|'age'
//类型变量k受T的约束 可以理解为 k只能是t所有键的任意一个
泛型接口与泛型类
泛型接口:
接口也可以配合泛型使用.
// 接口也可以配合泛型来使用 已增加灵活性 增强复用性
interface IdFunc<T>{
id:(Value:T)=>T
ids:()=>T[]
}
let Obj:IdFunc<string>={
id(Value){
return Value
},
ids(){
return []
}
}
泛型类:
class 也可以搭配泛型来用. 比如: react的class组件的基类 Component就是泛型 不用的组件有不同的props和state
//创建泛型类
class GenericNumber<NumType>{
defaultvalue: NumType;
constructor(value: NumType) {
this.defaultvalue = value;
}
add(x: NumType, y: NumType): NumType {
return (x as any) + (y as any);
}
}
//如果类存在构造函数并且构造函数正好使用到了类的泛型 就可以省略尖括号
联合类型与交叉类型
联合类型的使用和场景
联合类型与类型别名
为了简化复杂的联合类型,可以使用类型别名:
// 定义联合类型别名
type StringOrNumber = string | number;
type Status = "pending" | "approved" | "rejected";
let value: StringOrNumber;
value = 123;
value = "hello";
let status: Status;
status = "pending"; // 正确
status = "approved"; // 正确
status = "done"; // 错误,不在指定的字面量类型中
联合类型与类型守卫
当使用联合类型时,TypeScript只允许访问所有类型共有的属性和方法。要访问特定类型的属性,需要使用类型守卫:
function processValue(value: string | number) {
// 错误:length属性只存在于string类型中
// console.log(value.length);
// 使用类型守卫
if (typeof value === "string") {
// 在这个代码块中,TypeScript知道value是string类型
console.log(value.length); // 正确
console.log(value.toUpperCase());
} else {
// 在这个代码块中,TypeScript知道value是number类型
console.log(value.toFixed(2));
}
}
联合类型与接口
联合类型也可以与接口结合使用:
interface Bird {
type: "bird";
flyingSpeed: number;
}
interface Horse {
type: "horse";
runningSpeed: number;
}
// 联合类型
type Animal = Bird | Horse;
function moveAnimal(animal: Animal) {
switch (animal.type) {
case "bird":
console.log(`Bird flying at speed: ${animal.flyingSpeed}`);
break;
case "horse":
console.log(`Horse running at speed: ${animal.runningSpeed}`);
break;
}
}
联合类型与null/undefined
联合类型常用于处理可能为null或undefined的值:
// 用户可能未定义
let user: User | null = null;
// 在使用前需要检查
if (user !== null) {
console.log(user.name); // 安全访问
}
// 或者使用可选链操作符
console.log(user?.name);
联合类型与字面量类型
联合类型与字面量类型结合使用可以创建枚举式的类型:
// 方向只能是这四个字符串值之一
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction) {
// ...
}
move("up"); // 正确
move("north"); // 错误,不在指定的字面量类型中
联合类型的注意事项
- 只能访问共有成员:使用联合类型时,只能访问所有类型共有的属性和方法
- 类型守卫:要访问特定类型的属性,需要使用类型守卫进行类型检查
- 可读性:对于复杂的联合类型,建议使用类型别名提高可读性
- 过度使用:避免过度使用联合类型,可能导致代码难以维护
交叉类型:
1.定义
使用符合& 对两个接口进行组合 成一个新的类型
交叉功能类似于接口的继承 用来组合多个类型为一个类型(一般用在对象类型中)
//交叉功能类似于接口继承 用于组合多个类型为一个类型(常用于对象类型)
interface Person{
name:string
}
interface Contact{
phone:number;
}
type PersonContact =Person & Contact;
let obj:PersonContact={
name:'张三',
phone:123456789
}
2.接口交叉与继承
- 相同点: 都可以实现对象类型的组合
- 不同点:两种方式实现类型组合时 对于同名属性之间处理冲突的方式不同
//交叉类型和接口继承的对比
interface A {
fn:(vlaie:number)=>string;
}
//接口继承 出现这种情况要么接口会报错 要么只保留一个属性
interface B extends A {
fn(value:string):string
}
//交叉类型
interface A {
fn:(value:number)=>string;
}
interface B {
fn:(value:string)=>string;
}
type C = A & B;
//可以将组合后的c简单理解为 fn:(value:(number|string))=>string
处理方法: 对于接口继承要么类型会报错 要么只保留两个类型的其中之一 然后对于交叉合成来说 可以两个类型同时保留 类似于联合类型
第四部分:类型系统进阶
类型兼容性
结构化类型系统
ts使用的是结构化的类型系统 如果类的类型定义 是一样的 尽管类名是 不一样的 但是仍然可以当做一个类来看
class Point {
x:number;
y:number;
constructor(x:number,y:number) {
this.x = x;
this.y = y;
}
}
class Point2D {
x:number;
y:number;
constructor(x:number,y:number) {
this.x = x;
this.y = y;
}
}
let p1: Point =new Point2D(1,2) //这种写法是允许的 因为Point2D兼容Point 所以Point和Point2D可以看作是一个类
对象类型兼容
函数类型兼容
类型推断与类型断言
类型推断机制
类型断言的使用场景
映射类型与工具类型
索引签名类型
绝大多数情况下 我们都在使用对象前就确定的对象的结构 但是并未对象添准确的类型 索性签名类型就是为接口中的 索引 和 值 都进行类型标注
使用场景:无法确定对象中有哪些类型信息 此时就用索引签名类型
interface AnyObject{
[key:string]:number
}
let obj:AnyObject={
a:1,
b:2
}
//解释 使用[key:string] 用来约束接口中出现的属性名 表示只要是 string类型 的属性名称都可以出现在对象中
//:number约束了属性值的类型 表示只要是 number类型 的属性值都可以出现在对象中
//key只是一个占位符 有了[key:String]:number 就可以在对象中定义任意个属性 只要属性名是字符串 属性值是数字即可
//这里的key可以是任意名称
- 使用
[key:string]用来约束接口中出现的属性名 表示只要是string类型 的属性名称都可以出现在对象中 -
:number约束了属性值的类型 表示只要是number类型 的属性值都可以出现在对象中 -
key只是一个占位符 有了[key:String]:number就可以在对象中定义任意个属性 只要属性名是字符串 属性值是数字即可,这里的key可以是任意名称
映射类型
映射类型就是基于旧类型创建新类型(对象类型) 减少重复 提升开发效率
//例子
type Propkeys='x'|'y'|'z'
type Type1={
x:number;
y:number;
z:number;
}
//这样写将x y z重复写了两遍 通常可以使用映射类型来进行简化
type Type2={
[Key in Propkeys]:number
}
//实际开发还是使用Record类型工具
type Type3=Record<Propkeys,number>;
解释:
- 映射类型是基于索引签名类型的 所以语法类似于索引签名类型 也是用[]
-
[Key in Propkeys]表示遍历Propkeys中的每个元素 并将其赋值给Key - 映射类型不能用于接口 只能用于类型别名
- 实际开发还是使用
Record泛型工具
对象类型的类型映射
type Props={
a:number;
b:string;
c:boolean;
}
type Type={
[key in keyof Props]:number
}
let obj:Type={
a:1,
b:2,
c:3
}
泛型工具类型
Partial<Type>
用来构造一个类型 将type的所有属性设置为可选
-
interface Props{ //每个类型都是必选的属性 如果需要可选类型需要添加'?' id:string; children:number[] } type PartialProps=Partial<Props> // 创建的新类型结构和props一模一样 但是所有属性是可选的 const obj0:Props={ id:'1', // children:[1,2,3] }//缺少children属性 就会报错 const obj:PartialProps={ id:'1' }//可以只写一个属性
Readonly<type>
-
创建一个只读的类型 不可更改 就不需要单独为属性添加
readonly属性 -
type readonlyProps=Readonly<Props> const obj1:readonlyProps={ id:'1', children:[1,2,3] } obj1.id='2'//不可以修改
Pick<type,keys>
从type中选择一组属性来构造新类型
-
pick中有两个类型变量 如果值选择一个则值传入该属性名即可
-
第二个变量传图的属性只能是第一个类型变量中存在的属性
-
type PickProps=Pick<Props,'id'> const obj2:PickProps={ id:'1' //children:[1,2,3]//不可以添加 添加就会报错 } type PickProps = { id: string; }
Record<key,type>
构造一个对象类型 属性键为key 属性类型为type
-
type RecordObj=Record<'a'|'b',string> const obj3:RecordObj={ a:'1', b:'2' } //Record工具类型有两个类型变量 1.表示对象有哪些属性 2.表示对象属性对应的类型
第五部分:实用技巧与最佳实践
模块与声明文件
在ts文件中 有两种声明文件的方法 一个是后缀为.ts 一个是后缀为.d.ts
-
.ts文件 既包含类型信息又包含可执行代码 -
.d.ts文件 只包含类型信息 不包含可执行代码 用途是为js提供类型信息
类型声明文件概述
在开发的时候会使用很多第三方库 我们不知道这些库是用js写的还是ts写的 所以我们需要类型声明文件为已经存在的js库提供类型信息,这样我们在使用这些库的时候就可以获得类型检查和智能提示
使用第三方库的类型声明
可以使用npm i @types/库名 --save来安装库的类型信息(第三方库)
React 第五十二节 Router中 useResolvedPath使用详解和注意事项示例
前言
useResolvedPath 是 React Router v6 提供的一个实用钩子,用于解析给定路径为完整路径对象。
它根据当前路由上下文解析相对路径,生成包含 pathname、search 和 hash 的完整路径对象。
一、useResolvedPath 核心用途
- 路径解析:将相对路径解析为绝对路径
- 链接构建:安全地构建导航链接
- 路径比较:比较当前路径与目标路径
- 动态路由处理:正确处理嵌套路由中的路径
二、useResolvedPath 解析结果对象
useResolvedPath 返回一个包含以下属性的对象:
比如原路径是:const resolved = useResolvedPath('../users?id=123#profile')
// 返回内容为
{ pathname: '/users', search: '?id=123', hash: '#profile' }
-
pathname: 解析后的绝对路径 -
search: 查询字符串(如果有) -
hash: 哈希值(如果有)
三、useResolvedPath 基本用法示例
import { useResolvedPath } from 'react-router-dom';
function PathInfo() {
const resolved = useResolvedPath('../users?sort=name#section');
return (
<div>
<h3>路径解析结果</h3>
<p>原始路径: "../users?sort=name#section"</p>
<p>解析后路径名: {resolved.pathname}</p>
<p>查询参数: {resolved.search}</p>
<p>哈希值: {resolved.hash}</p>
</div>
);
}
四、useResolvedPath 实际应用场景
4.1、在面包屑导航中解析路径
import { useResolvedPath, Link, useLocation, useMatches } from 'react-router-dom';
function Breadcrumbs() {
const location = useLocation();
const matches = useMatches();
// 获取所有路由匹配项
const crumbs = matches
.filter(match => match.handle?.crumb)
.map(match => {
// 解析每个路由的路径
const resolvedPath = useResolvedPath(match.pathname);
return {
pathname: resolvedPath.pathname,
crumb: match.handle.crumb
};
});
return (
<nav className="breadcrumbs">
{crumbs.map((crumb, index) => (
<span key={index}>
{index > 0 && ' > '}
{index === crumbs.length - 1 ? (
<span className="current">{crumb.crumb}</span>
) : (
<Link to={crumb.pathname}>{crumb.crumb}</Link>
)}
</span>
))}
</nav>
);
}
// 在路由配置中使用
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
path: 'dashboard',
handle: { crumb: '控制面板' },
element: <Dashboard />,
children: [
{
path: 'stats',
handle: { crumb: '统计' },
element: <StatsPage />
}
]
},
{
path: 'users',
handle: { crumb: '用户管理' },
element: <UsersPage />
}
]
}
]);
4.2、创建自定义导航链接组件
import {
useResolvedPath,
useMatch,
Link
} from 'react-router-dom';
function CustomNavLink({ to, children, ...props }) {
const resolved = useResolvedPath(to);
const match = useMatch({ path: resolved.pathname, end: true });
return (
<div className={`nav-item ${match ? 'active' : ''}`}>
<Link to={to} {...props}>
{children}
</Link>
</div>
);
}
// 在导航中使用
function Navigation() {
return (
<nav>
<CustomNavLink to="/">首页</CustomNavLink>
<CustomNavLink to="/about">关于</CustomNavLink>
<CustomNavLink to="/products">产品</CustomNavLink>
<CustomNavLink to="/contact">联系我们</CustomNavLink>
</nav>
);
}
4.3、在嵌套路由中正确处理相对路径
import { useResolvedPath, Link, Outlet } from 'react-router-dom';
function UserProfileLayout() {
return (
<div className="user-profile">
<nav className="profile-nav">
<ProfileNavLink to=".">概览</ProfileNavLink>
<ProfileNavLink to="activity">活动</ProfileNavLink>
<ProfileNavLink to="settings">设置</ProfileNavLink>
<ProfileNavLink to="../friends">好友</ProfileNavLink>
</nav>
<div className="profile-content">
<Outlet />
</div>
</div>
);
}
function ProfileNavLink({ to, children }) {
const resolved = useResolvedPath(to);
const match = useMatch({ path: resolved.pathname, end: true });
return (
<Link
to={to}
className={match ? 'active' : ''}
>
{children}
</Link>
);
}
// 路由配置
const router = createBrowserRouter([
{
path: 'users',
element: <UsersLayout />,
children: [
{
path: ':userId',
element: <UserProfileLayout />,
children: [
{ index: true, element: <ProfileOverview /> },
{ path: 'activity', element: <ProfileActivity /> },
{ path: 'settings', element: <ProfileSettings /> }
]
},
{
path: ':userId/friends',
element: <UserFriends />
}
]
}
]);
4.4、动态生成侧边栏菜单
import { useResolvedPath, useMatch, Link } from 'react-router-dom';
function SidebarMenu({ items }) {
return (
<nav className="sidebar">
<ul>
{items.map((item) => (
<MenuItem key={item.path} to={item.path} label={item.label} />
))}
</ul>
</nav>
);
}
function MenuItem({ to, label }) {
const resolved = useResolvedPath(to);
const match = useMatch({ path: resolved.pathname, end: false });
return (
<li className={match ? 'active' : ''}>
<Link to={to}>{label}</Link>
{/* 显示子菜单(如果存在且匹配) */}
{match && resolved.pathname === to && (
<ul className="submenu">
<li><Link to={`${to}/details`}>详细信息</Link></li>
<li><Link to={`${to}/analytics`}>分析</Link></li>
</ul>
)}
</li>
);
}
// 使用示例
const menuItems = [
{ path: '/dashboard', label: '仪表盘' },
{ path: '/projects', label: '项目' },
{ path: '/reports', label: '报告' },
{ path: '/team', label: '团队' }
];
function AppLayout() {
return (
<div className="app-layout">
<SidebarMenu items={menuItems} />
<main className="content">
{/* 页面内容 */}
</main>
</div>
);
}
五、useResolvedPath 高级用法:路径比较工具
import { useResolvedPath, useLocation } from 'react-router-dom';
// 自定义钩子:比较当前路径是否匹配目标路径
function usePathMatch(to) {
const resolvedTo = useResolvedPath(to);
const location = useLocation();
// 创建当前路径对象(去除可能的尾部斜杠)
const currentPath = {
pathname: location.pathname.replace(/\/$/, ''),
search: location.search,
hash: location.hash
};
// 创建目标路径对象
const targetPath = {
pathname: resolvedTo.pathname.replace(/\/$/, ''),
search: resolvedTo.search,
hash: resolvedTo.hash
};
// 比较路径是否匹配
return (
currentPath.pathname === targetPath.pathname &&
currentPath.search === targetPath.search &&
currentPath.hash === targetPath.hash
);
}
// 在组件中使用
function NavigationItem({ to, children }) {
const isActive = usePathMatch(to);
return (
<li className={isActive ? 'active' : ''}>
<Link to={to}>{children}</Link>
</li>
);
}
六、 useResolvedPath 注意事项
6.1、相对路径解析
useResolvedPath 基于当前路由位置解析相对路径
6.2、查询参数和哈希
保留原始路径中的查询字符串和哈希值
6.3、动态路由参数
不会解析路径参数(如 :id),保持原样
6.4、性能考虑
解析操作轻量,但避免在循环中过度使用
6.5、路由上下文
必须在路由组件内部使用(在 <Router> 上下文中)
七、useResolvedPath 与相关钩子对比
总结
useResolvedPath 是 React Router v6 中处理路径的强大工具,主要用于:
- 在嵌套路由中正确处理相对路径
- 构建动态导航组件
- 创建面包屑导航等复杂导航结构
- 安全地比较路径和构建链接
通过合理使用 useResolvedPath,可以创建更健壮、可维护的路由结构,避免硬编码路径导致的错误,并简化嵌套路由中的路径处理逻辑。