简体   繁体   English

如何在 typescript 中定义动态通用约束

[英]How to define a dynamic generic constraint in typescript

I have a abstract base class with a generic type parameter like this:我有一个抽象的基础 class 具有这样的泛型类型参数:

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

I inherit multiple classes from the base.我从基础继承了多个类。 For example:例如:

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

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

Now I want to write a factory function that returns a new Base based on my input properties:现在我想写一个工厂 function 根据我的输入属性返回一个新的 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;
   ...
 } 
}

When passing 'string' as first parameter, the second can only be of type string.当传递 'string' 作为第一个参数时,第二个只能是字符串类型。 For 'decimal', type P can only be a number.对于“十进制”,类型 P 只能是数字。

But I have two problems.但我有两个问题。 When calling the function like this:当像这样调用 function 时:

getBase('string', '5');

It works, but it says that the signature is它有效,但它说签名是

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

It don't understand why it's not resolving to string but instead to the value of value?它不明白为什么它不解析字符串而是解析 value 的值?

The other problem is, that when I return a new TBase() it states that it could also be a string or boolean:另一个问题是,当我返回一个新的 TBase() 时,它指出它也可以是一个字符串或 boolean:

"'number' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint 'string | number | boolean'." “‘数字’可分配给‘P’类型的约束,但‘P’可以用约束‘字符串|数字|布尔’的不同子类型来实例化。”

I searched a lot about this, but couldn't get around it.我对此进行了很多搜索,但无法解决。 Could someone explain to me why excactly this happens?有人可以向我解释为什么会发生这种情况吗? Even when i explicit cast it to a Base it throws the error (return new TBase() as Base)即使我将它显式转换为 Base 也会引发错误(将 new TBase() 作为 Base 返回)

Another approach I tried was with using function overloads, but this looks kinda weird and not right:我尝试的另一种方法是使用 function 重载,但这看起来有点奇怪而且不正确:

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> {
  ...
}

I want to something like this:我想要这样的东西:


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

What am I missing?我错过了什么? I'm quiet struggling with this for a long time now.. Thanks in advance我很长一段时间都在为此苦苦挣扎。在此先感谢

Edit:编辑:

Playground Link 游乐场链接

I tried very hard to get your first approach to work in a fully type-safe manner without casts, but failed.我非常努力地让你的第一种方法在没有强制转换的情况下以完全类型安全的方式工作,但失败了。

This question also deals with return types whose types depend on argument types, and several answers suggest to use overloading. 这个问题还涉及其类型取决于参数类型的返回类型,并且有几个答案建议使用重载。 As you've already found, that's one way to solve it – although the return type of your implementation should not be Base<number | string | boolean>正如您已经发现的那样,这是解决它的一种方法——尽管您的实现的返回类型不应该是Base<number | string | boolean> Base<number | string | boolean> Base<number | string | boolean> but rather Base<number> | Base<string> | Base<boolean> Base<number | string | boolean>而是Base<number> | Base<string> | Base<boolean> Base<number> | Base<string> | Base<boolean> Base<number> | Base<string> | Base<boolean> . Base<number> | Base<string> | Base<boolean>

If you're okay with changing the call syntax a bit, you can do this and still remain fully type safe:如果您可以稍微更改调用语法,您可以这样做并且仍然保持完全类型安全:

// 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');

Some ways to extract types from this map:从这个 map 中提取类型的一些方法:

// 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>>;

Using these, you can in fact implement getBase in a rather ugly way:使用这些,您实际上可以以一种相当丑陋的方式实现getBase

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

I don't quite understand why the strange value as never is needed to make the compiler accept this, but it emits the right code in the end.我不太明白为什么要让编译器接受这个奇怪的value as never ,但它最终会发出正确的代码。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM