typescript在实际场景中的应用

316 阅读3分钟

前言

typescript只按照教程去学习,好像只会一些string、number等基本类型的使用。实际编程中,似乎总有一些场景不知道怎么去写类型,并且难以找到例子去学习。

本文列举的都是实际项目中用到的类型定义技巧,或许你能从中找到一些灵感。

实际场景中的TS类型应用

避免类型的重复定义

通常我们针对一个Table数据会定义如下类型Item,针对不同场景,需要重新定义某些属性,下面的一些TS内置类型,可以方便的实现:

/** 定义基本的数据项 */
type Item = {
  id: number;
  name: string;
  age: string;
  address?: string;
};

/** 列表数据,泛型的基本用法,也可以Item[] */
type List = Array<Item>;

/** 新增时表单数据,无id,使用Omit移除属性 */
type FormDataAdd = Omit<Item, 'id'>;

/** 修改时表单数据,只可修改address,使用Pick挑选属性 */
type FormDataModify = Pick<Item, 'id' | 'address'>;

const data: Item = {
  id: 1,
  name: 'xx',
  age: 18,
  address: 'xxx',
};
/** 当确定address必有值,使用NonNullable转换一下 */
const addressMustExist = data.address as NonNullable<Item['address']>;
/** 或者使用非空断言 */
const addressMustExist1 = data.address!;

/** 查询参数,使用Partial将属性转为可选,使用&扩展属性 */
type Query = Partial<List> & {
  current: number;
  pageSize: number;
};
/** 定义一个通用的service,泛型T 表示数据项,U 表示查询参数 */
type Service<T, U> = (params: U) => Promise<{
  data: T[];
  totalItem: number;
}>;

/** 运用 */
type PageService = Service<Item, Query>;

组件的二次封装

  • 基于antd的Input组件,封装自定义的MyInput
  • MyInput需要支持传入Input的所有props,除了disabled
  • disabled属性是根据myCustomProps动态计算的

主要使用了extends继承原有属性,Omit剔除部分属性

import { Input, InputProps } from 'antd';
import { useMemo } from 'react';

interface IProps extends Omit<InputProps, 'disabled'> {
  myCustomProps?: string;
}

export default function MyInput(props: IProps) {
  const { myCustomProps, ...restInputProps } = props;

  const computedDisabled = useMemo(() => {
    if (myCustomProps === 'xxx') {
      return true;
    }
    return false;
  }, [myCustomProps]);

  return <Input {...restInputProps} disabled={computedDisabled} />;
}

字符串格式限制

  • 写一个range方法,判断一个数字是否在某个区间内
  • 参数interval为string类型,如"[1,5]"表示x>=1且x<=5,"(2,3]"表示x>2且x<=3

校验了字符串的头尾字符限制,不过如果还要校验中间数字的话,还是需要代码逻辑去判断

代码演示

function range(num:number, interval: `${'[' | '('}${string}${')' | ']'}`) {
 // 实现省略...
}

ahooks的useAntdTable二次封装

useAntdTable用起来很方便,封装了常用的 Ant Design Form 与 Ant Design Table 联动逻辑,但是它对service数据格式有固定要求:

  1. service 接收两个参数,第一个参数为分页数据 { current, pageSize },第二个参数为表单数据。
  2. service 返回的数据结构为 { total: number, list: Item[] }

假如我们的service定义如下:

type Service<T> = (params: { currentPage: number; pageSize: number }) => Promise<{
  success: boolean;
  data: {
    data: T[];
    totalItem: number;
  };
}>;

使用起来就要做一些逻辑转换了:

const myService: Service = function () {
    return new Promise((resolve) => {
      resolve({
        success: true,
        data: {
          data: [],
          totalItem: 0,
        },
      });
    });
  };

  const { tableProps } = useAntdTable(async (params) => {
    const res = await myService({
      currentPage: params.current,
      pageSize: params.pageSize,
    });
    return {
      list: res.data.data,
      total: res.data.totalItem,
    };
  });

如果我们定义一个useMyAntdTable,service可以在内部转换后给useAntdTable,岂不是方便很多,但是类型要怎么处理呢?

先看的定义吧

declare const useAntdTable: <TData extends Data, TParams extends Params>(service: Service<TData, TParams>, options?: AntdTableOptions<TData, TParams>) => AntdTableResult<TData, TParams>;

关键点就在于泛型参数的处理:<TData extends Data, TParams extends Params>,直接贴代码:

import { useAntdTable } from 'ahooks';

import { AntdTableOptions, Data, Params } from 'ahooks/lib/useAntdTable/types';

/** 我们的返回数据类型 */
export type MyData = {
  success: boolean;
  data: {
    list: any[];
    totalItem: number;
  };
};
/** 我们的service入参 */
export type MyParams = {
  currentPage: number;
  pageSize: number;
  [key: string]: any;
};

/** 定义接受的service类型,extends关键词保证了service符合我们的定义约束 */
type MyService<TData extends MyData, TParams extends MyParams> = (args: TParams) => Promise<TData>;

/** 获取Promise的返回值类型,这里用到了infer,通常用来搭配泛型来获取某个具体的类型 */
type PromiseType<T> = T extends Promise<infer U> ? U : never;

/** 结合useAntdTable的类型定义,来修改我们要的类型 */
function useMyAntdTable<TData extends MyData, TParams extends MyParams>(
  service: MyService<TData, TParams>,
  options: AntdTableOptions<Data, Params> = {},
) {
  type returnType = PromiseType<ReturnType<typeof service>>;

  /**
   * 我们需要把对应类型也做转换,手动赋给useAntdTable的泛型参数,让TS识别到
   * 通常使用useAntdTable时并不需要指定这个泛型,是因为TS可以从service中推断出来
   */
  type TransformData = {
    list: returnType['data']['list'];
    total: number;
  };

  const hooks = useAntdTable<TransformData, Params>(async ({ current, pageSize }, params) => {
    const { data } = await service({
      currentPage: current,
      pageSize,
      ...params,
    });
    return {
      total: data.totalItem,
      list: data.list,
    };
  }, options);

  return hooks;
}

export default useMyAntdTable;

使用看看,代码简洁多了,dataSource类型也能正确提示出来:

image.png