如何在打字稿中定义动态泛型约束

时间:2021-02-02 10:52:37

标签: javascript typescript typescript-generics

我有一个带有泛型类型参数的抽象基类,如下所示:

abstract class Base<T> {
  constructor(protected value: T) {}
}

我从基础继承了多个类。例如:

class TBase extends Base<number> {
  constructor(value: number) { 
    super(value); 
  }
}

class XBase extends Base<string> {
  constructor(value: string) { 
    super(value); 
  }
}

现在我想编写一个工厂函数,根据我的输入属性返回一个新的 Base:

type ValidTypes = 'string' | 'integer' | 'decimal' | 'boolean' | 'datetime';

type validFor<T> = T extends 'string' | 'datetime'
  ? string
  : T extends 'integer' | 'decimal'
  ? number
  : T extends 'boolean'
  ? boolean
  : never;

function getBase<T extends ValidTypes, P extends validFor<T>>(type: T, value: P): Base<P> {
 switch(type) {
   case 'number': new TBase(coerceNumber(value)); break;
   ...
 } 
}

当传递 'string' 作为第一个参数时,第二个参数只能是 string 类型。对于 'decimal',输入 P 只能是数字。

但是我有两个问题。像这样调用函数时:

getBase('string', '5');

它有效,但它说签名是

function getBase<'string', '5'>(type: 'string', value: '5'): Base<'5'>

它不明白为什么它不解析为字符串而是解析为 value 的值?

另一个问题是,当我返回一个 new TBase() 时,它声明它也可以是一个字符串或布尔值:

"'number' 可分配给类型为 'P' 的约束,但可以使用约束 'string | number | boolean' 的不同子类型实例化 'P'。"

我对此进行了大量搜索,但无法绕过它。有人可以向我解释为什么会发生这种情况吗?即使我将它显式转换为 Base 它也会抛出错误(返回 new TBase() 作为 Base)

我尝试过的另一种方法是使用函数重载,但这看起来有点奇怪而且不对:

getBase(type: 'decimal' | 'integer', value: number): Base<number>
getBase(type: 'string' | 'datetime', value: string): Base<string>
getBase(type: 'boolean', value: boolean): Base<boolean>
getBase(type: ValidTypes, value: number | boolean | string): Base<number | boolean | string> {
  ...
}

我想要这样的东西:


getBase('string', 'thisMustBeAString'); // => Base<string>;
getBase('decimal', 54 /* Must be a number */) // => Base<number>;

我错过了什么?我很长时间都在为此苦苦挣扎..提前致谢

编辑:

Playground Link

1 个答案:

答案 0 :(得分:0)

我非常努力地让你的第一种方法以完全类型安全的方式工作,没有强制转换,但失败了。

This question 还处理类型取决于参数类型的返回类型,并且有几个答案建议使用重载。正如您已经发现的那样,这是解决问题的一种方法 - 尽管您的实现的返回类型不应该是 Base<number | string | boolean> 而是 Base<number> | Base<string> | Base<boolean>

如果您可以稍微更改调用语法,那么您可以这样做并且仍然保持完全类型安全:

// Deliberately leaving this untyped, so we get the most specific inferred type possible.
const factories = {
  'integer': (value: number) => new TBase(value) as Base<number>,
  'string': (value: string) => new XBase(value) as Base<string>,
}

const a: Base<string> = factories['string']('thisMustBeAString');
const b: Base<number> = factories['integer'](54);
const c: Base<number> = factories['integer']('thisWillNotCompile');
const d: Base<number> = factories['string']('neitherWillThis');
const e: Base<string> = factories[a.value]('norWillThis');

从该映射中提取类型的一些方法:

// Extract the names of the types: 'integer' | 'string'.
type TypeName = keyof typeof factories;

// Extract the type of the factory function, for example (value: number) => Base<number>.
type FactoryType<T extends TypeName> = typeof factories[T];

// Extract the type of argument that goes into the factory.
type FactoryArgumentType<T extends TypeName> = Parameters<FactoryType<T>>[0];

// Extract the factory's return type.
// Currently always equal to its argument type, but it doesn't need to be.
type FactoryReturnType<T extends TypeName> = ReturnType<FactoryType<T>>;

使用这些,您实际上可以以一种相当丑陋的方式实现 getBase

function getBase<T extends TypeName>(type: T, value: FactoryArgumentType<T>): FactoryReturnType<T> {
  return factories[type](value as never) as FactoryReturnType<T>;
}

我不太明白为什么需要奇怪的 value as never 才能让编译器接受它,但它最终会发出正确的代码。