Tìm kiếm trang web

typescript - Nhóm lồng nhau theo một hoặc nhiều cấp


Cho cấu trúc dữ liệu sau:

const data = [
  { A: 1, B: 12, C: 123 },
  { A: 1, B: 122, C: 1233 },
  { A: 2, B: 22, C: 223 }
];

Tôi muốn triển khai một hàm gọi là groupBy với chữ ký sau:

function groupBy<T, By extends keyof T>(object: T[], by: ...By[]): Map<By[0], Map<By[1], ..., Map<number, T[]>...>

mà tôi có thể gọi như sau:

const A = groupBy(data, "A"); // Map<number, { A: number, B: number, C: number }[]>;
const AB = groupBy(data, "A", "B"); // Map<number, Map<number, { A: number, B: number, C: number }[]>>;
const ABC = groupBy(data, "A", "B", "C"); // Map<number, Map<number, Map<number, ...>>;

Thật không may, tôi chỉ có thể triển khai groupBy với một cấp độ duy nhất:

function groupBy<T extends object, K extends keyof T>(collection: T[], iteratee: K): Map<T[K], T[]> {
  const map: Map<T[K], T[]> = new Map();

  for (const item of collection) {
    const accumalated = map.get(item[iteratee]);
    if (accumalated === undefined) {
      map.set(item[iteratee], [item]);
    } else {
      map.set(item[iteratee], [...accumalated, item]);
    }
  }

  return map;
}

Câu trả lời #1

Trước tiên hãy mô tả groupBy() ở cấp độ kiểu, bằng cách đưa ra chữ ký gọi có kiểu mạnh:

type GroupedBy<T, K> = K extends [infer K0, ...infer KR] ?
    Map<T[Extract<K0, keyof T>], GroupedBy<T, KR>> : T[];

// call signature
function groupBy<T, K extends Array<keyof T>>(
  objects: readonly T[], ...by: [...K]
): GroupedBy<T, K>;

Vì groupBy() là hàm tổng quát với T, kiểu của các phần tử trong mảng objects, cũng như K, một bộ khóa (tuple) của các khóa trong T tương ứng với tham số phần còn lại ...by. Hàm trả về GroupedBy<T, K>.

Vậy GroupedBy<T, K> là gì? Đây là một kiểu điều kiện đệ quy. Nếu tuple K rỗng, thì nó chỉ là T[] (vì gom một mảng theo rỗng sẽ cho ra mảng giống hệt). Ngược lại, chúng ta sử dụng các kiểu tuple biến đổi để tách tuple K thành phần tử đầu tiên, K0, và phần còn lại, KR. Sau đó, GroupedBy<T, K> sẽ là một Map với kiểu khóa là kiểu của thuộc tính của T tại khóa K0 (về khái niệm, đây chỉ là một kiểu truy cập theo chỉ mục T[K0], nhưng trình biên dịch không biết rằng K0 sẽ là khóa của T, nên chúng ta dùng kiểu tiện ích Extract<T, U> để thuyết phục nó... nên T[Extract<K0, keyof T>]), và kiểu giá trị là, theo đệ quy, GroupedBy<T, KR>.

Hãy chắc chắn rằng trình biên dịch thực hiện đúng cách:

const A = groupBy(data, "A"); 
// Map<number, { A: number, B: number, C: number }[]>;

const AB = groupBy(data, "A", "B"); 
// Map<number, Map<number, { A: number, B: number, C: number }[]>>;

const ABC = groupBy(data, "A", "B", "C"); /* Map<number, Map<number, Map<number, {
    A: number;
    B: number;
    C: number;
}[]>>> */

Trông ổn. Chỉ để chắc chắn, hãy đổi number sang cái gì đó khác:

const otherData = [
    { str: "a", num: 1, bool: true },
    { str: "a", num: 1, bool: false },
    { str: "a", num: 2, bool: true }
];
const grouped = groupBy(otherData, "str", "num", "bool")
/* const grouped: Map<string, Map<number, Map<boolean, {
    str: string;
    num: number;
    bool: boolean;
}[]>>> */

Cũng trông tốt.


Bây giờ hãy triển khai groupBy(). Trình biên dịch không thể theo dõi được kiểu điều kiện đệ quy bên trong phần triển khai (xem microsoft/TypeScript#33912), vì vậy hãy nới lỏng bằng cách biến nó thành một hàm nạp chồng với một chữ ký gọi duy nhất có kiểu mạnh, và một chữ ký triển khai lỏng lẻo sử dụng kiểu any. Chúng ta phải cẩn thận để làm đúng vì trình biên dịch sẽ không phát hiện ra lỗi kiểu.

Dù sao, đây là một cách triển khai khả thi:

// implementation
function groupBy(objects: readonly any[], ...by: Array<PropertyKey>) {
    if (!by.length) return objects;
    const [k0, ...kr] = by;
    const topLevelGroups = new Map<any, any[]>();
    for (const obj of objects) {
        let k = obj[k0];
        let arr = topLevelGroups.get(k);
        if (!arr) {
            arr = [];
            topLevelGroups.set(k, arr);
        }
        arr.push(obj);
    }
    return new Map(Array.from(topLevelGroups, ([k, v]) => ([k, groupBy(v, ...kr)])));

}

Nếu mảng by trống, trả về mảng objects mà không thay đổi; điều này tương ứng với trường hợp cơ bản của GroupedBy<T, K> trong đó K là []. Ngược lại, chúng ta chia by thành phần tử đầu tiên k0 và phần còn lại kr. Sau đó, chúng ta đi qua và thực hiện việc nhóm cấp cao của objects theo các giá trị trong khóa k0 của chúng. Điều này bao gồm việc đảm bảo rằng chúng ta khởi tạo các mảng trước khi đưa các phần tử vào chúng. Và cuối cùng, ở cuối, chúng ta biến đổi nhóm cấp cao đó (sử dụng kỹ thuật này), đệ quy, áp dụng groupBy() cho từng mảng các đối tượng.

Hãy xem nó có hoạt động không:

console.log(A);
/* Map (2) {1 => [{
  "A": 1,
  "B": 12,
  "C": 123
}, {
  "A": 1,
  "B": 122,
  "C": 1233
}], 2 => [{
  "A": 2,
  "B": 22,
  "C": 223
}]}  */

console.log(AB);
/* Map (2) {1 => Map (2) {12 => [{
  "A": 1,
  "B": 12,
  "C": 123
}], 122 => [{
  "A": 1,
  "B": 122,
  "C": 1233
}]}, 2 => Map (1) {22 => [{
  "A": 2,
  "B": 22,
  "C": 223
}]}}  */

Cũng trông tốt.

Liên kết Playground đến mã

Bài viết liên quan