0

0

利用TypeScript泛型与接口实现HTTP服务模拟数据精确类型推断教程

DDD

DDD

发布时间:2025-09-02 15:40:01

|

646人浏览过

|

来源于php中文网

原创

利用TypeScript泛型与接口实现HTTP服务模拟数据精确类型推断教程

本教程旨在解决TypeScript在通用HTTP服务模拟中数据类型推断不精确的问题。通过深入探讨TypeScript的泛型、字面量类型(as const)和可辨识联合类型,我们将展示如何构建一个能够根据请求URL精确推断返回数据具体形状的HttpServiceMock。教程将提供两种实现方案:基于数组的方案和基于对象表的方案,并附带详细代码示例和原理分析,帮助开发者充分利用TypeScript的强大类型系统。

1. 问题背景:通用模拟服务中的类型推断挑战

前端开发和测试中,我们经常需要模拟http服务来解耦前后端开发或编写单元测试。一个常见的模式是创建一个通用的httpservicemock,它接受一个包含模拟数据的数组,并根据请求的url返回相应的数据。然而,在使用typescript时,我们可能会遇到一个挑战:尽管typescript能够识别返回值的整体类型(例如promise),但当t是一个泛型类型且模拟数据数组中包含多种不同形状的数据时,typescript往往无法精确推断出特定url对应数据的具体结构,导致属性被标记为可选或类型过于宽泛。

考虑以下初始实现:

interface HttpServiceMockData {
  status: number;
  data: T;
  url: string;
}

export function createHttpServiceMock(data: HttpServiceMockData[]) {
  return {
    get: async (url: string): Promise<{ data: T }> => {
      const res = data.find((d) => d.url === url);
      if (!res) {
        throw new Error(`No data found for url ${url}`);
      }
      return {
        data: res.data,
      };
    },
  };
}

// 使用示例
const service = createHttpServiceMock([
  {
    url: '/users/1',
    data: {
      id: 1,
      username: 'test',
    },
    status: 200,
  },
  {
    url: 'test',
    data: {
      id: 1,
      username: 'test',
      lastname: 'test',
    },
    status: 200,
  },
]);

service.get('test').then((res) => {
  // 此时,res.data 的类型是 { id: number; username: string; lastname?: string; }
  // TypeScript 将 lastname 推断为可选属性,因为并非所有模拟数据都包含它。
  // 我们希望当 url 为 'test' 时,res.data 能够精确推断出 { id: number; username: string; lastname: string; }
  console.log(res.data.lastname); // 可能提示 lastname 是可选的
});

在这个例子中,createHttpServiceMock 函数的泛型参数T被推断为所有data对象中data属性的联合类型,这导致了lastname属性被标记为可选。为了解决这个问题,我们需要更精确地指导TypeScript,使其能够根据传入的URL字面量来推断出对应的具体数据类型。

2. 解决方案一:利用泛型、字面量类型和可辨识联合类型

要实现精确的类型推断,我们需要利用TypeScript的以下高级特性:

  • 字面量类型(Literal Types)与 as const 断言: 通过将url属性标记为 as const,TypeScript会将其类型推断为具体的字符串字面量(例如'/users/1'),而不是宽泛的string类型。这是构建可辨识联合类型的基础。
  • 泛型(Generics): 允许我们在函数或类中使用类型变量,增加代码的灵活性和类型安全性。
  • 可辨识联合类型(Discriminated Unions): 当一个联合类型中的每个成员都含有一个共同的、具有字面量类型的属性时,TypeScript可以通过这个属性来区分联合类型的不同成员。
  • 交叉类型(Intersection Types): & 操作符用于将多个类型合并成一个新类型,新类型将包含所有合并类型的成员。

下面是改进后的createHttpServiceMock函数实现:

interface HttpServiceMockData {
  status: number;
  data: T;
  url: U; // 将 url 类型参数化
}

export function createHttpServiceMock>(
  data: ReadonlyArray
) {
  return {
    get: async (url: TargetUrl)
        : Promise<{ data: (Services & { url : TargetUrl })['data'] }> => {
      // 运行时实现保持不变,类型推断在编译时完成
      const res = (data as Services[]).find((d) => d.url === url);
      if (!res) {
        throw new Error(`No data found for url ${url}`);
      }
      return {
        data: res.data as (Services & { url : TargetUrl })['data'], // 进行类型断言以匹配返回类型
      };
    },
  };
}

代码解析:

  1. HttpServiceMockData 接口:
    • 我们为url属性引入了一个新的泛型参数U,并约束其为string的子类型。这为后续的字面量类型推断做准备。
  2. createHttpServiceMock> 函数:
    • Services 是一个泛型参数,它代表了传入data数组中所有HttpServiceMockData对象的联合类型。extends HttpServiceMockData 确保Services是HttpServiceMockData的某种形式。
    • data: ReadonlyArray:表示data是一个只读的Services类型数组。ReadonlyArray确保数组内容不会被修改,并且允许TypeScript更好地推断数组元素的类型。
  3. get: async (url: TargetUrl) 方法:
    • TargetUrl extends Services['url']:这是一个关键点。Services['url']会提取Services联合类型中所有url属性的字面量类型,形成一个新的字面量联合类型(例如:'/users/1' | 'test')。TargetUrl被约束为这个联合类型的一个成员,这意味着当我们调用get('test')时,TargetUrl的类型就是字面量'test'。
  4. Promise 返回类型:
    • Services & { url : TargetUrl }:这是一个交叉类型。它将整个Services联合类型与一个具有特定url字面量类型(即TargetUrl)的对象类型进行交叉。由于Services是一个可辨识联合类型(其url属性是辨识器),TypeScript能够通过{ url: TargetUrl }精确地从Services联合类型中筛选出匹配的那个成员。
    • ['data']:最后,我们从筛选出的具体服务类型中提取其data属性的类型。

使用 as const 断言:

为了让TypeScript将url属性推断为字面量类型,而不是宽泛的string,我们需要在定义模拟数据时使用as const断言。

Red Panda AI
Red Panda AI

AI文本生成图像

下载
const service = createHttpServiceMock([
  {
    url: '/users/1' as const, // 明确将 url 声明为字面量类型
    data: {
      id: 1,
      username: 'test',
    },
    status: 200,
  },
  {
    url: 'test' as const, // 或者直接将整个对象声明为 as const
    data: {
      id: 1,
      username: 'test',
      lastname: 'test',
    },
    status: 200,
  },
]);

service.get('test').then((res) => {
  // 此时,res.data 的类型将精确推断为 { id: number; username: string; lastname: string; }
  console.log(res.data.lastname); // 不再提示可选,类型安全
});

service.get('/users/1').then((res) => {
  // 此时,res.data 的类型将精确推断为 { id: number; username: string; }
  // console.log(res.data.lastname); // 报错:Property 'lastname' does not exist on type '{ id: number; username: string; }'
});

通过这种方式,我们成功地利用了TypeScript的强大类型系统,实现了根据URL精确推断返回数据形状的目标。

3. 解决方案二:基于对象表(Service Table)的实现

如果你的模拟服务配置更适合用一个对象而不是数组来表示,那么可以采用基于对象表的方案。这种方式可以简化类型推断,因为它天然地将URL作为键,将服务配置作为值,使得类型查找更加直观。

type ServiceTable = { [K: string]: HttpServiceMockData };

export function createHttpServiceMockTable(
  data: Services
) {
  return {
    get: async (url: TargetUrl)
        : Promise<{ data: Services[TargetUrl]['data'] }> => {
      // 运行时实现
      const res = data[url];
      if (!res) {
        throw new Error(`No data found for url ${url}`);
      }
      return {
        data: res.data as Services[TargetUrl]['data'], // 类型断言
      };
    },
  };
}

// 使用示例
const service2 = createHttpServiceMockTable({
  '/users/1': {
    url: '/users/1',
    data: {
      id: 1,
      username: 'test',
    },
    status: 200,
  },
  'test': {
    url: 'test',
    data: {
      id: 1,
      username: 'test',
      lastname: 'test',
    },
    status: 200,
  },
} as const); // 同样需要 as const 来确保键是字面量类型

service2.get('test').then((res) => {
  // 此时,res.data 的类型将精确推断为 { id: number; username: string; lastname: string; }
  console.log(res.data.lastname);
});

service2.get('/users/1').then((res) => {
  // 此时,res.data 的类型将精确推断为 { id: number; username: string; }
  // console.log(res.data.lastname); // 报错
});

代码解析:

  1. ServiceTable 类型: 定义了一个索引签名,表示键是字符串,值是HttpServiceMockData对象。
  2. createHttpServiceMockTable 函数: Services泛型直接代表了传入的整个服务配置对象。
  3. get: async (url: TargetUrl) 方法:
    • TargetUrl extends keyof Services:keyof Services会提取Services对象的所有键的字面量联合类型(例如'/users/1' | 'test')。TargetUrl被约束为这个联合类型的一个成员。
  4. Promise 返回类型:
    • Services[TargetUrl]:TypeScript的索引访问类型(Indexed Access Types)可以直接根据TargetUrl这个字面量键从Services对象类型中获取对应的服务配置类型。
    • ['data']:然后从获取到的服务配置类型中提取data属性的类型。

这种基于对象表的方案在类型推断上更为直观和简洁,因为它直接利用了JavaScript对象的键值对结构。同样,为了让keyof Services能够精确地推断出字面量键,传入的配置对象也需要使用as const断言。

4. 总结与注意事项

  • as const 的重要性: 无论是哪种方案,as const断言都是实现精确字面量类型推断的关键。它告诉TypeScript将变量或属性推断为最窄的字面量类型,而不是更宽泛的基本类型(如string或number)。
  • 泛型与类型约束: 合理使用泛型和类型约束是编写灵活且类型安全的TypeScript代码的基础。它们允许函数处理多种类型,同时保持类型信息的精确性。
  • 可辨识联合类型与交叉类型: 在处理包含多种可能性的数据结构时,可辨识联合类型结合交叉类型是强大的工具,能够帮助TypeScript在运行时逻辑的基础上进行编译时类型缩小。
  • 索引访问类型: 对于对象结构,索引访问类型(如Services[TargetUrl])提供了一种直接通过键来获取对应值类型的方式,非常适用于基于键值对的类型查找场景。
  • 运行时与编译时: TypeScript的类型系统主要在编译时发挥作用。虽然我们通过类型体操实现了精确的类型推断,但运行时代码的逻辑仍然需要确保能够正确处理数据。例如,find方法或对象属性访问仍需考虑找不到数据的情况。在示例中,我们使用了类型断言(as ...)来辅助运行时代码与编译时类型保持一致,但在实际生产代码中,应尽可能通过更安全的类型守卫或类型保护来避免不必要的断言。

通过本教程,我们深入探讨了如何利用TypeScript的泛型、字面量类型、可辨识联合类型和索引访问类型,解决了通用HTTP服务模拟中数据类型推断不精确的问题。掌握这些高级特性将极大地提升你在复杂应用中构建健壮、类型安全代码的能力。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

541

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

372

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

727

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

470

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

391

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

990

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

653

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

543

2023.09.20

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

0

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.1万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 1.9万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 2.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号