如何完美的支持一个链式调用函数的ts声明?

467 阅读2分钟

假设我们有一个帮助函数来辅助我们创建一个对象。那么如何才能让这个函数最后返回的值是一个 键值完整 的对象呢?

一、举例说明

上代码

//目标对象:
type TargetType = {
    name: string,
    age: number,
    salary: number
}

// 示例代码
const data: Record<string, any> = {}

const builder = {
    add(key: string, value: any) {
        data[key] = value
        return builder
    },
    build() {
        return data
    }
}

const result = builder.add('name', 'haoza').add('age', 30).add('salary', 2233).build()

type Type = typeof result

// 请问 Type 的类型是什么呢?
// 当然毫无疑问是一个 Record<string, any>

image.png 上述代码最终 Type 肯定是 Record<string, any>,毕竟 data 的类型就是这个

那么如何才能得到我们期望的类型呢?

二、借助体操函数

type Builder<
  T extends Record<string, any> = Record<string, never>,
> = {
  add: <N extends string, I extends any>(
    name: N,
    item: I,
  ) => Builder<T & { [k in N]: I }>;

  build: () => T;
};

/**
 * 完备的类型校验
 */
export const BuilderCreator = (): Builder => {
  const items: Record<string, any> = {};

  const builder = {

    add: (name: string, val: any) => {
      items[name] = val;
      return builder;
    },

    build: () => {
      // 类型推到已经由体操函数支持,所以使用any不影响类型推断
      return items as any;
    },
  };
  return builder;
};



const builder = BuilderCreator()


const result = builder.add('name', 'haoza').add('age', 30).add('salary', 2233).build()

image.png

image.png

非常完美!不过有两个个关键点可以注意下

Builder 的范型参数默认值用的是 Record<string, never>

BuilderCreator 是一个普通函数而不是类class,因为使用类的实例化可能会导致嵌套层级过深而报错

三、检查key是否重复

一般来说重复传入key很容易导致bug,那么我们该如何限制呢?关键就在于如何判断 key 已经存在和抛出错误



type Builder<
  T extends Record<string, any> = Record<string, never>,、
  // 增加了新的参数
  F extends string = '',
> = {
  add: <N extends string, I extends any>(
    // 判断重复则产生 never
    name: N & (N extends F ? '重复的属性,请注意修改' : N),
    item: I,
  ) => Builder<T & { [k in N]: I }, F | N>;

  build: () => T;
};

/**
 * 完备的类型校验
 */
export const BuilderCreator = (): Builder => {
  const items: Record<string, any> = {};

  const builder = {

    add: (name: string, val: any) => {
      items[name] = val;
      return builder;
    },

    build: () => {
      // 类型推到已经由体操函数支持,所以使用any不影响类型推断
      return items as any;
    },
  };
  return builder;
};



const builder = BuilderCreator()


const result = builder.add('name', 'haoza').add('name', 'hao')

image.png

最后再把 add 函数中 val 的类型改成项目中真实的约束,快去装X了~