了解和实践 Typescript 的泛型 (Generics)

考虑如下场景

我们现在自己封装了一个 Table 组件,该组件提供 datacolumns 两个属性,我们希望用户在使用组件时,编辑器能针对 datacolumn的值类型进行约束,而不是任意的一个值, 如下图所示:


public render() {
   const users = [{ name: 'ziv', birth: "1991" }];
   const columns = [
      { Key: "id", name: "ID", dataIndex: "id" },
      { key: "name", name: "姓名", dataIndex: "name" },
   ];
   return (
      <div>
         <Table data={users} columns={columns}/>
      </div>
   );
}

正如上图所示,我columns中取数据的dataIndex字段,在传入的 users列表中是不存在的,所以结果是数据无法正常获取。 那么有没有办法在代码执行前就检测出这个问题呢?让我们看看用泛型如何解决这个问题。

泛型的基本语法

泛型(Generic)提供了一种在消费API对象时按需添加约束类型的能力,从而不必固定在某个单一的类型约束上,而是按使用的实际情况时具体约束

基本用法如下:

函数(function)

语法

function A<T> {}
function B<T,K> {}

使用

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

接口(interface & type)

语法:

// interface
interface A<T> {}
interface B<T,K> {}

// type
type A<T> = {}
type B<T, K> = {};

使用:

export interface ColumnProps<T> { // 声明泛型 T
    key: number|string;
    name: string;
    dataIndex: keyof T; // 约束 dataIndex 值需为引用泛型 T 中的属性
}

类(class)

语法:

class A<T> {}
class B<T, K> {};

使用:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

接上文中的场景

回到刚才我们封装的 Table 这个示例中,我们针对columndata添加泛型约束,我们看看具体如何使用:

import * as React from "react";

export interface ColumnProps<T> { // 声明泛型 T
    key: number|string;
    name: string;
    dataIndex: keyof T; // 约束 dataIndex 值需为引用泛型 T 中的属性
}

interface TableProps<T> { // 声明泛型 T
    data: T[];  // 约束 data 数组为 T 类型数组
    columns: Array<ColumnProps<T>> ; // dataIndex 应该是泛型 T 中的属性
}

class Table<T> extends React.Component<TableProps<T>, any> { // 声明泛型 T,TableProps 引用 T
    public render() {
        return (
            <div>
                <table>
                    {this.renderHeader()}
                    {this.renderBody()}
                </table>
            </div>
        );
    }
    private renderHeader = () => {
        const { columns } = this.props;
        return (
            <thead>
                <tr>
                    {columns.map((col: ColumnProps<T>) => <th key={col.key}>{col.name}</th>)}
                </tr>
            </thead>
        );
    }

    private renderBody = () => {
        const { data, columns } = this.props;
        const getTd = (item: T) => { // item 的类型应该为T
            // dataIndex 应该是泛型 T 中的属性
            return columns.map((col: ColumnProps<T>, index: number) => (
                <td key={`${index}-${col.dataIndex}`}>{item[col.dataIndex]}</td>
            ));
        };
        return (
            <tbody>
                {data.map((item: T, index: number) => (<tr key={`${index}`}>{getTd(item)}</tr>))}
            </tbody>
        );
    }
}

export default Table;

使用时大致如下:

import Table, { ColumnProps } from "../components/table";

export interface User {
    id?: number | string;
    username?: string;
    password?: string;
    email?: string;
    gender?: string;
    age?: number;
}

public render() {
    const users = [{ name: "ziv", birth: '1991' }];
    const columns: Array<ColumnProps<User>> = [
        { Key: "id", name: "ID", dataIndex: "id" }, // invalid, Key 并属于 ColumnProps 类型, 应该是 key
        { key: "name", name: "姓名", dataIndex: "sex" }, // invalid, sex 属性并不存在于 User 类型,
    ];
    return (
        <div>
            <Table<User> data={users} columns={columns}/> // invalid, 数组对象 users 并不是于 User 数组类型
        </div>
    );
}

很显然上面的datacolumn需要按UserColumnProps的约束来传值,否则编译器就会抛出类型错误。

再看一个栗子,使用泛型 封装个 HTTP 工具类

首先,我们声明一个 Http 接口类型


export default interface HttpInterface {
    /**
     * HTTP Get method
     * @param url request URL
     * @param params  request Parameter
     */
    get<R, P = {}>(url: string, params?: P): Promise<R>;
    /**
     * HTTP Post method
     * @param url request URL
     * @param body request body object
     */
    post<R, P = {}>(url: string, body?: P): Promise<R>;
    /**
     * Post an object as a formData object
     * @param url request URL
     * @param params the params object that wait to convert to formData
     */
    postAsFormData<R, P = {}>(url: string, params?: P): Promise<R>;
    /**
     * Post a form element
     * @param url request URL
     * @param form HTML Form element
     */
    postForm<R>(url: string, form: HTMLElement): Promise<R>;
    /**
     * Http request
     * @param url request URL
     * @param options request options
     */
    request<R>(url: string, options?: RequestInit): Promise<R>;
}

然后用一个类(class)实现 Http 接口:


import "whatwg-fetch";

import HttpInterface from "./interface";

class Http implements HttpInterface {

    public get<R, P = {}>(url: string, params?: P): Promise<R> {
        const newUrl: string = params ? this.build(url, params) : url;
        return this.request<R>(newUrl, {
            method: "GET",
        });
    }
    public post<R, P = {}>(url: string, body?: P): Promise<R> {
        const options: RequestInit = { method: "POST" };
        if (body) {
            options.body = JSON.stringify(body);
        }
        return this.request<R>(url, options);
    }
    public postAsFormData<R, P = {}>(url: string, params?: P): Promise<R> {
        const options: RequestInit = { method: "POST" };
        if (params) {
            options.body = this.buildFormData(params);
        }
        return this.request<R>(url, options);
    }
    public postForm<R>(url: string, form: HTMLFormElement): Promise<R> {
        const options: RequestInit = { method: "POST" };
        if (form) {
            options.body = new FormData(form);
        }
        return this.request<R>(url, options);
    }
    public request<R>(url: string, options?: RequestInit): Promise<R> {
        options.credentials = "same-origin";
        return fetch(url, options)
        .then<R>((response) => {
            return response.json();
        })
        .catch( (err) => {
            return err;
        });
    }

    public build(url: string, params: any) {
        const ps = [];
        if (params) {
            for (const p in params) {
                if (p) {
                    ps.push(p + "=" + encodeURIComponent(params[p]));
                }
            }
        }
        return url + "?" + ps.join("&");
    }

    public buildFormData(params: any) {
        if (params) {
            const data = new FormData();
            for (const p in params) {
                if (p) {
                    data.append(p, params[p]);
                }
            }
            return data;
        }
    }
}

export default Http;

代码中,我们分别使用泛型RP分别来约束请求返回数据类型和请求参数类型, 所以当我们在调用时便可以按我们约定的类型来返回数据:

import HttpImpl from "../utils/http";

const Http = new HttpImpl();

function getUsers() {
    return Http.get<User[]>("/user"); // 返回 Promise<User[]>
}

function updateUser(user: User) {
    return Http.post<User>("/user/login", user); // Promise<User>
}

一些使用技巧(Tips)

  • 泛型继承
function merge<T extends object>(obj: T) {}
  • 继承某个泛型对象的属性
function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}
  • 设置默认泛型值
function func<T=string>(obj: T) {}
function func<T=User&{ name: string }>(obj: T) {}

最后

使用泛型(Generics)显然对我们的一些通用的对象或者组件提供了一种非常灵活的类型约束方式,某些情况下明确的类型(甚至使用 any )约束可能会是更好的选择,在实际应用中我们更多的是需要站在使用API使用者的角度来设计这些类型约束。


最后修改于 2019-07-25