TypeScript 类型系统详解(四)

250 阅读17分钟

在 TypeScript 中,类型系统是确保代码质量的关键特性之一。通过类型注解和类型推断,开发者可以构建出既灵活又安全的应用。本文对 TypeScript 类型系统的深入探讨,是系列文章的第四篇。

1. 泛型中的类型推断

使用泛型之后,我们的类型推断的入口就多了起来,如下代码所示,即使我们没有显示的传入泛型类型,但 Ts 仍然成功推断出了对应的类型。

class ArrayOfAnything<T> {
  constructor(public collection: T[]) {}

  get(index: number): T {
    return this.collection[index];
  }
}

我们显式的指定 T 的类型,和我们在构造函数中隐式的指定,效果是相同的。

const arr = new ArrayOfAnything<string>(["a", "b", "c"]);
console.log(arr.get(1));

等价于,

const arr = new ArrayOfAnything(["a", "b", "c"]);
console.log(arr.get(1));

但是需要注意的是,显式指定的类型是能够覆盖隐式推断的,因此,下面的代码是会报错的:

const arr = new ArrayOfAnything<number>(["a", "b", "c"]); // Type 'string' is not assignable to type 'number'.ts(2322)
console.log(arr.get(1));

泛型显式声明的位置一般都在标识符和后面的括号之间,使用 <*> 表示,比如:

  1. 泛型数组:

    function duplicate<T extends number>(blueprint: T) {
      let genericArray: Array<T> = [blueprint, blueprint];
      return genericArray;
    }
    
  2. 泛型函数:

    function identity<T>(arg: T): T {
      return arg;
    }
    
  3. 泛型类:

    class GenericNumber<T extends number> {
      value: T;
      constructor(value: T) {
        this.value = value;
      }
    }
    
  4. 泛型接口:

    interface GenericIdentityFn<T> {
      (arg: T): T;
    }
    
  5. 泛型数组工具函数:

    function map<T, U>(array: T[], transform: (item: T) => U): U[] {
      return array.map(transform);
    }
    
  6. 泛型栈:

    class Stack<T> {
      private items: T[] = [];
      push(item: T): void {
        this.items.push(item);
      }
      pop(): T | undefined {
        return this.items.pop();
      }
    }
    
  7. 泛型队列:

    class Queue<T> {
      private items: T[] = [];
      enqueue(item: T): void {
        this.items.push(item);
      }
      dequeue(): T | undefined {
        return this.items.shift();
      }
    }
    
  8. 泛型链表节点:

    class ListNode<T> {
      value: T;
      next: ListNode<T> | null;
      constructor(value: T) {
        this.value = value;
        this.next = null;
      }
    }
    
  9. 泛型二叉搜索树:

    class BinarySearchTree<T extends Comparable> {
      // BST methods with T requiring a compareTo method
    }
    
  10. 泛型哈希表:

    class HashMap<K, V> {
      private buckets: Array<Array<[K, V]>> = [];
      // HashMap methods...
    }
    
  11. 泛型图:

    class Graph<T> {
      private adjacencyList: Map<T, T[]> = new Map();
      // Graph methods...
    }
    
  12. 泛型优先队列:

    class PriorityQueue<T> {
      private elements: T[] = [];
      // PriorityQueue methods with custom comparator
    }
    
  13. 泛型字典:

    class Dictionary<K, V> {
      private entries: Map<K, V> = new Map();
      set(key: K, value: V): void {
        this.entries.set(key, value);
      }
      get(key: K): V | undefined {
        return this.entries.get(key);
      }
    }
    

每个示例中,尖括号 <T><K, V> 表示泛型参数,T 代表任意类型,KV 分别代表键和值的类型(通常用在映射或字典中)。使用泛型可以使得数据结构更加灵活,能够适应不同的数据类型需求。

2. 受限泛型

泛型虽然能够极大的拓展我们定义的数据结构的使用范围,但是有一个很大的问题就是它能代表的范围实在是太宽了。有的时候管的太宽并不是一件好的事情,必须给予其约束。

interface Printable {
  print():void;
}

function printHousesOrCars<T extends Printable>(arr: T[]):void {
  for(let i=0;i<arr.length;i++){
    arr[i].print();
  }
}

printHouses0rCars<House>([new House(), new House()]);
printHouses0rCars<Car>([new Car(),new Car()]);

由于 House 和 Car 都实现了 Printable 接口,因此是不会报错的。

T extends Printable 限制了泛型 T 不可以恣意妄为,底线是在其中实现 print 方法。这种限制看似削弱了泛型的范围,实则让其变得更加有用,所以本质上还是对其增强。

3. 完全受限的泛型

上一小节中,我们使用了 extends 对泛型 T 进行了约束,但是还有个问题。extends 只是约束了泛型的下限,也就是说其必须实现 print 方法,但是没有限制其可不可以拥有其它 100 个其它的方法和属性,那么这种 上限 的约束应该如何实现呢?见下面的代码:

type UpperLimit = {
  name: string,
  sex: boolean,
  age: number,
  print(text: string): void;
}

type LowerLimit = {
  name: string,
}

type WithinLimits<T> = UpperLimit extends T ? LowerLimit : never;

function test<T extends WithinLimits<T>>(_: T): void {
  console.log('T is: ', _);
}

const _a = {
  name: '250',
}

const _b = {
  name: '250',
  sex: true,
}

const _c = {
  name: '250',
  sex: true,
  employer: '38',
}

test(_a);
test(_b);
test(_c); // Argument of type '{ name: string; sex: boolean; employer: string; }' is not assignable to parameter of type 'never'.

在上面的代码中,我们使用了泛型别名的方式实现了两头堵的目的:type WithinLimits<T> = UpperLimit extends T ? LowerLimit : never;

4. 可选类型

在定义接口 interface 的时候,我们可以使用 ?: 表示某个属性或者方法是可选实现的。

interface UserProps {
  name?: string;
  age?: number;
}

5. 函数的类型别名

下面罗列两个常见的函数别名:

  1. type Callback = () => {};
  2. type Callback = () => void;

6. field 类型约束

或者说是对 key 的类型约束。Javascript 中的 object 的 key 实际上是可以由三种类型的:string number symbol, 因此有必要对其进行类型约束:

type Callback = () => void;
events:{ [key: string]: Callback[] } = {};

这里我们使用了 ES6 可变 key 的语法。其中 key 不是关键,可以是任意字符,例如:

events:{ [key1: string]: Callback[] } = {};
events:{ [ke2y: string]: Callback[] } = {};
events:{ [k3ey: string]: Callback[] } = {};

7. json-server 的使用

简介

json-server 是一个轻量级的 REST API 服务器,它使用一个简单的 JSON 对象来存储数据,并提供了一套完整的 CRUD (创建、读取、更新、删除) 操作。它非常适合用于快速原型开发、测试或学习 RESTful API。

安装

首先,您需要安装 json-server。通过 npm 全局安装是最简单的方法:

npm install -g json-server

启动服务器

创建一个 db.json 文件,这将作为我们的数据库。例如:

{
  "users": [
    { "id": 1, "name": "Alice", "age": 30 },
    { "id": 2, "name": "Bob", "age": 25 }
  ]
}

使用 json-server 启动服务器:

json-server --watch db.json

这将启动一个默认监听在 3000 端口的服务器。

RESTful 接口实现

json-server 为我们提供了以下 RESTful API 接口:

  1. 列出所有资源: 使用 axios 获取用户列表:

    import axios from 'axios';
    
    async function getAllUsers() {
      try {
        const response = await axios.get('http://localhost:3000/users');
        console.log(response.data);
      } catch (error) {
        console.error('Error fetching users:', error);
      }
    }
    
  2. 根据 ID 获取单个资源: 使用 axios 获取特定用户:

    async function getUserById(id: number) {
      try {
        const response = await axios.get(`http://localhost:3000/users/${id}`);
        console.log(response.data);
      } catch (error) {
        console.error(`Error fetching user with id ${id}:`, error);
      }
    }
    
  3. 创建新资源: 使用 axios 创建新用户:

    async function createUser(user: { name: string, age: number }) {
      try {
        const response = await axios.post('http://localhost:3000/users', user);
        console.log(response.data);
      } catch (error) {
        console.error('Error creating user:', error);
      }
    }
    
  4. 更新资源: 使用 axios 更新用户信息:

    async function updateUser(id: number, updates: Partial<{ name: string, age: number }>) {
      try {
        const response = await axios.put(`http://localhost:3000/users/${id}`, updates);
        console.log(response.data);
      } catch (error) {
        console.error(`Error updating user with id ${id}:`, error);
      }
    }
    
  5. 删除资源: 使用 axios 删除用户:

    async function deleteUser(id: number) {
      try {
        const response = await axios.delete(`http://localhost:3000/users/${id}`);
        console.log(response.data);
      } catch (error) {
        console.error(`Error deleting user with id ${id}:`, error);
      }
    }
    

自定义路由

json-server 也支持自定义路由。在命令行中使用 --router 选项指定自定义路由文件:

json-server --watch db.json --router routes.json

中间件

json-server 允许你使用中间件来处理请求前后的逻辑。中间件的使用方式与 Express.js 类似。

总结

json-server 是一个非常有用的工具,可以快速搭建 RESTful API 服务器,无需编写后端代码。它不仅简化了开发流程,还为开发者提供了一个学习 RESTful API 的优秀环境。本文介绍了如何使用 json-server 以及如何通过 axios 进行网络通信,提供了 TypeScript 代码示例。记住,json-server 主要用于开发和测试阶段。对于生产环境,你应该使用更强大的后端解决方案。

8. Ts 中的两种 class

在 TS 中,我们一般将所有的类分成两种:Modal Classes 和 View Classes.

  1. Model Classes: Handle data, used to represent Users, Blog Posts, lmages, etc. -- 数据类
  2. View Classes: Handle HTML and events caused by the user (like clicks). -- 视图类

创建一个典型的数据类 -- User Class

对于一个典型的数据类,它可能需要满足如下的要求:

  1. 存储用户数据,例如姓名和年龄。
  2. 提供数据的存储、检索和修改功能。
  3. 在数据发生变化时,能够通知应用程序的其他部分。
  4. 能够将数据持久化到外部服务器,并在将来某个时刻检索这些数据。

接下来,我们实现这个类,我们的实现思路如下:

  1. 在一个 class 种实现上述所有要求,将所有的属性和方法都堆在一起。
  2. 使用 Composition 设计模式优化我们的代码。
  3. 拓展 User 类,使之成为一个通用数据类,而不是仅表示用户数据。

以列表方式呈现的 User 类的数据和功能概述:

  • 类名:User

  • 私有属性:

    • data: UserProps (存储用户信息的对象)
  • 方法:

    • get(propName: string): string | number: 获取用户信息的单个属性(如名称或年龄)。
    • set(update: UserProps): void: 更改用户信息(如名称和年龄)。
    • on(eventName: string, callback: () => {}): 注册事件处理器,以便应用程序的其他部分在数据变化时得到通知。
    • trigger(eventName: string): void: 触发事件,告知应用程序的其他部分数据已更改。
    • fetch(): Promise: 从服务器获取特定用户的数据。
    • save(): Promise: 将用户数据保存到服务器。
  • 功能:

    1. 存储有关特定用户的信息(名称、年龄)。
    2. 获取有关此用户的单个信息(名称、年龄)。
    3. 更改有关此用户的信息(名称、年龄)。
    4. 注册此对象的事件处理器,以便应用程序的其他部分在某些数据变化时得到通知。
    5. 触发事件,告知应用程序的其他部分发生了变化。
    6. 从服务器获取特定用户的数据。
    7. 将有关此用户的数据保存到服务器。

首先我们在同一个类中实现所有的功能:

import axios, { AxiosPromise } from 'axios';

interface UserProps {
  name?: string;
  age?: number;
  id?: string; // 假设每个用户都有一个唯一的标识符
}

type Callback = () => void;

export class User {
  private data: UserProps;
  private events: { [eventName: string]: Callback[] } = {};

  constructor(data: UserProps) {
    this.data = data;
  }

  get(propName: string): string | number | undefined {
    return this.data[propName];
  }

  set(update: UserProps): void {
    this.data = { ...this.data, ...update };
    // 假设每次更改数据都会触发 "change" 事件
    this.trigger('change');
  }

  on(eventName: string, callback: Callback): void {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  trigger(eventName: string): void {
    const handlers = this.events[eventName];
    if (handlers && handlers.length > 0) {
      handlers.forEach(callback => callback());
    }
  }

  fetch(): Promise<void> {
    const id = this.get('id');
    return axios.get(`http://localhost:3000/users/${id}`)
      .then((response: AxiosResponse<UserProps>) => {
        this.set(response.data);
      })
      .catch(error => {
        console.error('Error fetching user:', error);
      });
  }

  save(): Promise<AxiosPromise<UserProps>> {
    const id = this.get('id');
    const url = id ? `http://localhost:3000/users/${id}` : 'http://localhost:3000/users';
    return id
      ? axios.put(url, this.data)
      : axios.post(url, this.data);
  }
}

上述代码中有 3 个点需要注意:

  1. axios.get 的 then 方法中的参数函数的形参类型是固定的:response: AxiosResponse<UserProps>.
  2. axios.post 或者 axios.put 函数的返回值类型固定为 Promise<AxiosPromise<UserProps>>.
  3. 一般来说,对于列表数据,如果有 id 说明是在更新此 id 的信息,走的是 put 接口;而如果没有 id 说明是新增的数据(brand new),走的是 post 接口。

应用 Composition 设计模式将相关逻辑剥离出来

User 类抽离完成之后形如:

  • class 名称:User
    • attribute: Attributes -- Gives us the ability to store properties tied to this user (name, age, etc).
    • events: Events -- Gives us the ability to tell other parts of our application whenever data tied to a particular user is changed.
    • sync: Sync -- Gives us the ability to save this persons data to a remote server, then retrieve it in the future.

1. 形变

对比下面几段代码:

// 第一种
export class MatchReader {
  
  static fromCsv(filename: string): MatchReader {
    return new MatchReader(new CsvFileReader(filename));
  }

  constructor(public reader: DataReader) {}
}

// 第二种
export class MatchReader {

  public reader: DataReader;

  constructor(filename: string) {
    this.reader = new CsvFileReader(filename);
  }
}

// 第三种
export class MatchReader {
  constructor(public reader: DataReader) {}
}

第一种和第三种比较,第一种进步很大,因为第一种封装了构造细节,是构造参数从 object 类型降维到 string 类型。而第三种和第二种比较也有较为明显的优势,如果使用第一种写法,那么后续就算有修改也不会改变构造函数,直接修改静态函数即可,这保证了类作为 blueprint 的稳定性,并且第一种写法的构造函数本身就比较简单。

2. 剥离发布订阅相关功能

要使用组合(Composition)设计模式抽离发布订阅功能,我们可以创建一个单独的类 Eventing,它将包含 ontriggerevents 存储逻辑。然后,我们将在 User 类中包含 Eventing 的实例,而不是在 User 类中直接实现这些方法。以下是如何实现这一点的示例代码:

首先,定义 Eventing 类:

export class Eventing {
  events: { [eventName: string]: Callback[] } = {};

  on(eventName: string, callback: Callback): void {
    const handlers = this.events[eventName] || [];
    handlers.push(callback);
    this.events[eventName] = handlers;
  }

  trigger(eventName: string): void {
    const handlers = this.events[eventName];
    if (handlers && handlers.length > 0) {
      handlers.forEach(callback => callback());
    }
  }
}

然后,修改 User 类以使用 Eventing 类:

import axios, { AxiosPromise, AxiosResponse } from 'axios';

interface UserProps {
  name?: string;
  age?: number;
  id?: string;
}

type Callback = () => void;

export class User {
  private data: UserProps;
  private eventing: Eventing; // 包含 Eventing 类的实例

  constructor(data: UserProps) {
    this.data = data;
    this.eventing = new Eventing(); // 实例化 Eventing
  }

  // 使用 Eventing 实例的方法
  on(eventName: string, callback: Callback): void {
    this.eventing.on(eventName, callback);
  }

  trigger(eventName: string): void {
    this.eventing.trigger(eventName);
  }

  // 其他 User 类的方法保持不变
  // ...
}

当然下面的代码还可以写成多种形式,不论是静态方法也好还是硬编码也好:

  constructor(data: UserProps) {
    this.data = data;
    this.eventing = new Eventing(); // 实例化 Eventing
  }

User 类中,我们添加了一个新的私有成员 eventing,它是 Eventing 类的一个实例。然后,我们将 ontrigger 方法委托给这个实例。这样,User 类就通过组合拥有了发布订阅功能,而不是通过继承。

请注意,我已经修正了您提供的抽离代码中的一些语法错误,并添加了缺失的部分。现在,User 类和 Eventing 类是分离的,User 类通过组合模式使用 Eventing 类的功能。这种方式使得代码更加模块化,易于维护和扩展。

3. 剥离后端通信相关功能

上一小节中我们成功的将发布订阅功能以 Composition 的方式承包给了其它实例完成,在这个小节中,我们使用相同的手法处理和网络通信相关的功能,也就是原来类中的 fetch 和 save 方法。

在正式开始优化之前有一个问题需要搞清楚,那就是与网络通信我们只能使用基本数据类型,也就是 Javascript 中的对象必须序列化之后才能通过网络进行传输,同理从网络过来的数据也必须经过反序列化之后才能参与到后续的代码逻辑之中。

  1. Serialize: Convert data from an obiect into some save-able format(ison).
  2. PutDeserialize: data on an object using some previously saved data (json).

关于序列化和反序列化,有一点小细节。如下代码所示:

const a = {name:'zs'};
const b = JSON.stringify(a);
const d = JSON.parse(b);
d.__proto__; // {__defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, __lookupSetter__: ƒ, …}
d.__proto__.__proto__; // null
const e = Object.create(null, {});
e.name = 'zs';
e.__proto__; // undefined

上面的代码想要说的是 JSON.parse 得到的结果并不是一张白纸,它的结果实际上等同于通过字面量创建的 object 一样。

那么我们使用序列和反序列化做出来的结果就是:

// User 类,实现了 Serializable 接口
class User implements Serializable {
  // 实现序列化方法,返回对象的序列化形式
  serialize(): {} {
    // 序列化逻辑
  }
}

// 可反序列化的接口,定义了一个反序列化方法
interface Deserializable {
  deserialize(json: {}): void;
}

// Sync 类,包含数据持久化的方法
class Sync {
  // 保存数据的方法,接受一个 ID 和一个可序列化的对象
  save(id: number, serializable: Serializable): void {
    // 保存逻辑
  }

  // 获取数据的方法,接受一个 ID 和一个可反序列化的对象
  fetch(id: number, deserial: Deserializable): void {
    // 获取逻辑
  }
}

那么上述的解决方案有什么问题呢?实际上,这并不是好的实践,因为在序列化和反序列化的过程中,我们丢失了对有效信息的类型约束,我们的 typescript 变成了 anyscript 所以这是不可接受的。

利用 AxiosPromise 泛型

我们可以利用 Axios 提供的名为 AxiosPromise 的泛型解决上述问题,这个时候我们可以将接口写成:

import axios, { AxiosPromise } from 'axios';

// 定义 Sync 泛型类,其中 T 是数据类型,AxiosPromise<T> 是 Axios 的 Promise 类型
class Sync<T> {
  // save 方法接受一个 ID 和一个类型为 T 的数据对象,返回一个 AxiosPromise<T>
  save(id: number, data: T): AxiosPromise<T> {
    // 这里应该是调用 Axios 或其他 HTTP 客户端库保存数据的逻辑
    // 例如: return axios.post(`/api/data/${id}`, data);
  }

  // fetch 方法接受一个 ID,返回一个 AxiosPromise<T>,表示获取的数据
  fetch(id: number): AxiosPromise<T> {
    // 这里应该是调用 Axios 或其他 HTTP 客户端库获取数据的逻辑
    // 例如: return axios.get<T>(`/api/data/${id}`);
  }
}

据此,我们可以将另外一个承包商 Sync 类实现为:

import axios, { AxiosPromise, AxiosResponse } from 'axios';  
  
interface UserProps {  
  name?: string;  
  age?: number;  
  id?: string;  
}  
  
interface HasId {  
  id?: string; // 改为 string 类型以匹配 UserProps 中的 id 类型  
}  
  
class Sync<T extends HasId> {  
  constructor(public rootUrl: string) {}  
  
  fetch(id: string): AxiosPromise {  
    return axios.get(`${this.rootUrl}/${id}`);  
  }  
  
  save(data: T): AxiosPromise {  
    const { id } = data;  
    if (id) {  
      return axios.put(`${this.rootUrl}/${id}`, data);  
    } else {  
      return axios.post(this.rootUrl, data);  
    }  
  }  
}  
  
type Callback = () => void;  
  
export class User {  
  private data: UserProps;  
  private events: { [eventName: string]: Callback[] } = {};  
  private sync: Sync<UserProps>;  
  
  constructor(data: UserProps, sync: Sync<UserProps>) {  
    this.data = data;  
    this.sync = sync;  
  }  
  
  get(propName: string): string | number | undefined {  
    return this.data[propName];  
  }  
  
  set(update: UserProps): void {  
    this.data = { ...this.data, ...update };  
    this.trigger('change');  
  }  
  
  on(eventName: string, callback: Callback): void {  
    if (!this.events[eventName]) {  
      this.events[eventName] = [];  
    }  
    this.events[eventName].push(callback);  
  }  
  
  trigger(eventName: string): void {  
    const handlers = this.events[eventName];  
    if (handlers && handlers.length > 0) {  
      handlers.forEach(callback => callback());  
    }  
  }  
  
  fetch(): Promise<void> {  
    const id = this.get('id');  
    if (id) {  
      this.sync.fetch(id).then((response: AxiosResponse<UserProps>) => {  
        this.set(response.data);  
      }).catch(error => {  
        console.error('Error fetching user:', error);  
      });  
    }  
  }  
  
  save(): Promise<void> {  
    return this.sync.save(this.data).then(() => {  
      console.log('User saved successfully');  
    }).catch(error => {  
      console.error('Error saving user:', error);  
    });  
  }  
}

4. 剥离获取和赋值功能

也就是将原来类中的 set 和 get 功能外包出去。代码相当简单,如下所示:

// Attributes.ts  
export class Attributes<T> {  
  constructor(private data: T) {}  
  
  get(propName: string): any {  
    return this.data[propName];  
  }  
  
  set(update: T): void {  
    Object.assign(this.data, update);  
  }  
  
  getAll(): T {  
    return this.data;  
  }  
}  
  
// User.ts  
import axios, { AxiosResponse } from 'axios';  
import { Attributes } from './Attributes';  
  
interface UserProps {  
  name?: string;  
  age?: number;  
  id?: string;  
}  
  
type Callback = () => void;  
  
export class User {  
  private attributes: Attributes<UserProps>;  
  private events: { [eventName: string]: Callback[] } = {};  
  
  constructor(data: UserProps) {  
    this.attributes = new Attributes(data);  
  }  
  
  get(propName: string): string | number | undefined {  
    return this.attributes.get(propName);  
  }  
  
  set(update: UserProps): void {  
    this.attributes.set(update);  
    this.trigger('change');  
  }  
  
  getAll(): UserProps {  
    return this.attributes.getAll();  
  }  
  
  on(eventName: string, callback: Callback): void {  
    if (!this.events[eventName]) {  
      this.events[eventName] = [];  
    }  
    this.events[eventName].push(callback);  
  }  
  
  trigger(eventName: string): void {  
    const handlers = this.events[eventName];  
    if (handlers && handlers.length > 0) {  
      handlers.forEach(callback => callback());  
    }  
  }  
  
  fetch(): Promise<void> {  
    const id = this.get('id');  
    return axios.get(`http://localhost:3000/users/${id}`)  
      .then((response: AxiosResponse<UserProps>) => {  
        this.set(response.data);  
      })  
      .catch(error => {  
        console.error('Error fetching user:', error);  
      });  
  }  
  
  save(): Promise<void> {  
    const id = this.get('id');  
    const url = id ? `http://localhost:3000/users/${id}` : 'http://localhost:3000/users';  
    return axios[id ? 'put' : 'post'](url, this.getAll())  
      .then(() => {  
        console.log('User saved successfully');  
      })  
      .catch(error => {  
        console.error('Error saving user:', error);  
      });  
  }  
}

8. 高级类型限制

上一个小节最后部分的代码中,有一处瑕疵:

  get(propName: string): string | number | undefined {  
    return this.attributes.get(propName);  
  }  

问题在于,当我使用 const id = this.get['id'] 的时候,我得到的结果的类型是 string | number | undefined 但是它只有可能是 number | undefined 为了保证类型系统的正确性,我还需要写如下的类型守卫代码:

if(typeof id  === 'number' ){
  //
}

为了解决这个问题,我们需要预知两个观点:

  1. 在 Ts 中, string 可以作为类型使用;
type BestName = 'stephen';
const printName = (name: BestName): void => {

}
printName('stephen'); // right
printName('Alex'); // wrong
  1. 在 Js 或者 Ts 中,所有的 object 的键一般都是 string 类型的。
  2. 上面两条结合起来的结论就是,在 Ts 中 object 的键可以作为类型。

对于下面所示的接口:

interface UserProps {
  name: string;
  age: number;
  id: number;
}

我们通过 typescript 中的 keyof 操作符就可以将 UserProps 中的 key 提取成联合类型:

interface UserProps {
  name: string;
  age: number;
  id: number;
}

type keys = keyof UserProps; // 'name' | 'age' | 'id'

function printKey (key: keys): void {
  console.log(key)
}

printKey('name');
printKey('age');
printKey('id');
printKey('sex'); // Argument of type '"sex"' is not assignable to parameter of type 'keyof UserProps'.

根据此,我们就可以用下面的方法解决上面的问题:

export class Attributes<T> {  
  constructor(private data: T) {}  
  
  get<K extends keyof T>(key: K): T[K] {  
    return this.data[key];  
  }  
  
  set(update: T): void {  
    Object.assign(this.data, update);  
  }  
  
  getAll(): T {  
    return this.data;  
  }  
}  

上面代码中,泛型 K 是 泛型 T 的衍生,而通过 T[K] 可以取到其值类型。

总的来说,这是一个相当常用的高级技巧!<K extends keyof T>(key: K): T[K].

9. Composition 中的代理

所谓的代理,指的就是:

  get(propName: string): string | number | undefined {  
    return this.attributes.get(propName);  
  }  

这样我们就可以直接使用 instance.get('id') 了,而不是使用 instance.attributes.get('id')

但是这样写不高级,高级的写法是使用 getter:

  get get() {  
    return this.attributes.get;  
  }  

但是这又牵扯到 this 的指向问题,所以在 Attribute 类内部需要手动的固定 this 的指向,我们可以通过多种方式进行:

export class Attributes<T> {  
  constructor(private data: T) {}  
  
  get = <K extends keyof T>(key: K): T[K] => {  
    return this.data[key];  
  }  
  
  ... 
}  

或者,

export class Attributes<T> {  
  constructor(private data: T) {
    this.get = this.get.bind(this);
  }  
  
  get<K extends keyof T>(key: K): T[K] {  
    return this.data[key];  
  }  
  
  ... 
}  

10. refactor 之后的 User 类

使用 Composition 改进之后的 User 类如下所示:

import axios, { AxiosPromise, AxiosResponse } from 'axios';

// 首先,引入其它类
import Eventing from './path-to-Eventing'; // 假设 Eventing 类在 path-to-Eventing 路径下
import Sync from './path-to-Sync'; // 假设 Sync 类在 path-to-Sync 路径下
import Attributes from './path-to-Attributes'; // 假设 Attributes 类在 path-to-Attributes 路径下

interface UserProps {
  name?: string;
  age?: number;
  id?: string;
}

class User {
  private attributes: Attributes<UserProps>;
  private sync: Sync<UserProps>;
  private events: Eventing;

  constructor(data: UserProps) {
    this.attributes = new Attributes(data);
    this.sync = new Sync<UserProps>('http://localhost:3000/users');
    this.events = new Eventing();
  }  
  
  get get() {
    return this.attributes.get;
  }

  get on() {
    return this.events.on;
  }

  get trigger() {
    return this.events.trigger;
  }
  
  set(update: UserProps): void {
    this.attributes.set(update);
    this.trigger('change');
  }

  fetch(): Promise<void> {
    const id = this.get('id');
    if (!id) {
      return Promise.reject(new Error('User ID is required to fetch data.'));
    }
    return this.sync.fetch(id).then((response: AxiosResponse<UserProps>) => {
      this.set(response.data);
    }).catch(error => {
      console.error('Error fetching user:', error);
      throw error;
    });
  }

  save(): Promise<AxiosPromise<UserProps>> {
    return this.sync.save(this.attributes.getAll());
  }
}

export default User;