问题描述

为下面描述的map函数添加一个更好的Typescript类型标注,提高使用者的类型安全性(如果不按下面的方式使用,就会报错):

map是一个用来做数组mapping的高阶函数,先接收mapping function(将【输入数组的项】映射为【输出数组的项】),然后接收输入数组,返回输出数组。
当mapping function没有传入时,mapping function默认为一个不做任何转化的函数(即x=>x)。

用代码来表示,就是:

const map = (fn = x => x) => arr => {
  const result = [];
  for (const item of arr) {
    result.push(fn(item));
  }
  return result;
};

Typescript Playground

方案1:函数重载


function map(): <V>(arr: V[]) => V[];
function map<V, R>(fn: (value: V) => R): (arr: V[]) => R[];
function map(fn = (x: any) => x) {
  return (arr: any[]) => {
    const result: any[] = [];
    for (const item of arr) {
      result.push(fn(item));
    }
    return result;
  };
}

// 测试

const emptyMap = map();
const result1 = emptyMap([1, 2, 3]);
const result2 = emptyMap(["1", "2"]);

const mapNumberToString = map((v: number) => "str");
const result3 = mapNumberToString([1, 2, 3]);
// const result4 = mapNumberToString(["1", "2"]); // 报错,类型检查有效
// const mapNumberToString2 = map((v: number) => "str", [1, 2]); // 报错,类型检查有效

方案2:类型推导

事实上类型推导也能够做到(虽然可维护性下降很多),就当做是对Typescript类型推导的一个练习吧!

真实项目中遇到这种情况,应该选择可维护性更高的方式。
type FnBasic = (value: any) => any;

type InferV<Fn> = Fn extends ((value: infer V) => any) ? V : never;
type InferR<Fn> = Fn extends ((value: any) => infer R) ? R : never;

const map = <Args extends [FnBasic?]>(...args: Args) => {
  // 通过tuple types来拿到可选参数的实际类型
  // https://stackoverflow.com/a/51489032
  type FnType = Args[0];
  const fn: FnBasic = args[0] || (x => x);
  type ReturnType = FnType extends (undefined)
    ? <V>(arr: V[]) => V[] // 返回一个带泛型的函数,从而V能够在被调用时确定
    : (arr: InferV<FnType>[]) => InferR<FnType>[]; // V已经能够推导出来

  const ret: ReturnType = ((arr: any[]) => {
    const result = [];

    for (const item of arr) {
      result.push(fn(item));
    }

    return result;
  }) as any;

  return ret;
};

// 测试

const emptyMap = map();
const result1 = emptyMap([1, 2, 3]);
const result2 = emptyMap(["1", "2"]);

const mapNumberToString = map((v: number) => "str");
const result3 = mapNumberToString([1, 2, 3]);
// const result4 = mapNumberToString(["1", "2"]); // 报错,类型检查有效
// const mapNumberToString2 = map((v: number) => "str", [1, 2]); // 报错,类型检查有效

其中主要的知识点是:

  1. 利用tuple types来拿到可选参数的实际类型(如果不用这个方式,可选参数的类型很难用来做类型推导)
  2. 基于参数类型推导出想要的返回值类型(涉及到infer的使用)

csRyan
1.1k 声望198 粉丝

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.