如何在扩展的类型参数上设置类型文字(或防止类型扩展)?

时间:2019-08-14 18:33:56

标签: typescript types type-inference

我想创建一个生成强类型对象的“工厂”的函数。这些对象有一个名为“ tag”的属性,它是一个string,但我想将此字符串设为string literal

这是实现某种“ AbstractFactory”模式所必需的,换句话说,就是一组创建对象的函数,该对象创建所需的对象,如下所示:

//Over Simplified Version

type Factory = {
  a: (name: string) => { tag: name };
  b: (name: string) => { tag: name };
};

type Names = {
  a: "A";
  b: "B";
};

type Result = {
  a: { tag: "A" };
  b: { tag: "B" };
};

function(factory): (names) => result

要注意的是,这个“标签”对象具有额外的属性,因此我需要此字符串文字才能正确使用它们。

我已经用这个概念进行了各种实验,我得到的是:

// Implementation

type Tag<T extends string> = { tag: T };

type Factory<
  TKey extends string,
  TName extends string,
  TTag extends Tag<TName>
> = {
  [key in TKey]: (name: TName) => TTag;
};

type Names<TFactory extends Factory<any, any, any>, TName extends string> = {
  [key in keyof TFactory]: TName;
};

type Result<
  TFactory extends Factory<any, any, any>,
  TNames extends Names<TFactory, any>
> = {
  /**
   * I am almost sure that the problem lies here, maybe because I am
   * not passing the required generic, I just can't figure it out.
   */
  [key in keyof TNames]: ReturnType<TFactory[key]>;
};

function implementation<
  TKey extends string,
  TName extends string,
  TTag extends Tag<TName>,
  TFactory extends Factory<TKey, TName, TTag>,
  TNames extends Names<TFactory, TName>,
  TResult extends Result<TFactory, TNames>
>(factory: TFactory): (names: TNames) => TResult {
  return (names) => {
    const keys = Object.keys(factory) as Extract<keyof TFactory, string>[];
    return keys.reduce((result, key) => {
      const name: TName = names[key];
      (result as Record<string, TTag>)[key] = factory[key](name);
      return result;
    }, {}) as TResult;
  };
}

const factory = implementation({
  a: (name: string) => ({ tag: name, index: 2 }),
  b: (name: string) => ({ tag: name, name: "Bob" }),
});

const result = factory({ a: "A", b: "B" } as const/* names */);

result.a.index; // the index type got inferred properly
result.b.name; // the name type got inferred properly

result.a.tag; // the tag type got widened to string, I want to be string literal "A"
result.b.tag; // the tag type got widened to string, I want to be string literal "B"

我认为我有点过度使用泛型。我很确定有一种更简单的方法。

TL; DR:我想防止将字符串文字扩展为string,因此我的tag属性成为我提供的文字。

1 个答案:

答案 0 :(得分:1)

可能有比这更理想的解决方案,因为我通常更喜欢避免TypeScript中的类型相交(&)。并不是说它们一定是不好的做法,而是与使用其他方法相比,它们在编辑器的工具提示中往往产生不太可读的类型提示。

interface Tag<T extends string> {
  tag: T;
}

interface Factory {
  (name: string): Tag<string>;
}

interface FactoryMapper {
  [key: string]: Factory;
}

type Names<TMapper extends FactoryMapper> = {
  [TKey in keyof TMapper]: string;
}

type Result<TMapper extends FactoryMapper, TNames extends Names<TMapper>> = {
  [TKey in keyof TMapper]: ReturnType<TMapper[TKey]> & Tag<TNames[TKey]>;
}

function implementation<TMapper extends FactoryMapper> (mapper: TMapper) {
  return <TNames extends Names<TMapper>> (names: TNames) => (
    Object.keys(mapper).reduce<Partial<Result<TMapper, TNames>>>(
      (result, key) => Object.assign(
        result,
        { [key]: mapper[key](names[key]) }
      ),
      {}
    ) as Result<TMapper, TNames>
  );
}

const factory = implementation({
  a: name => ({ tag: name, index: 2 }),
  b: name => ({ tag: name, name: "Bob" }),
});

const result = factory({ a: "A", b: "B" } as const);

result.a.index; // the index type got inferred as number
result.b.name; // the name type got inferred as string

result.a.tag; // the tag type got inferred as "A"
result.b.tag; // the tag type got inferred as "B"

此解决方案的关键部分是Result类型的定义:

type Result<TMapper extends FactoryMapper, TNames extends Names<TMapper>> = {
  [TKey in keyof TMapper]: ReturnType<TMapper[TKey]> & Tag<TNames[TKey]>;
}

使用相交来缩小所得成员的定义。上面的其余代码也应有助于举例说明如何更好地编写泛型的类型参数。