打字稿模板文字类型循环约束

Typescript template literal types circular constraint

我正在努力寻找一种在错误消息中显示类型参数的方法。这个想法是为了防止传递已经注入的依赖项,并在编译时检查它。

我是这样解决的:

export type TCons<T> = new (...args: any[]) => T

export interface Has<K extends string, T> {
  get: (k: K, v: TCons<T>) => T
}

type CombineExclusive<Host, Key extends string, Provider> = <
  P extends Host extends Has<Key, P> ? `${Key} already exists` : Provider
>(
  provider: P
) => Host extends Has<Key, P> ? never : Host & Has<Key, P>

export interface Application {
  withSearchProvider: CombineExclusive<this, "SearchProvider", SearchProvider>
}

如果这样使用:

const liveApplication = (app: Application) =>
  app
    .withSearchProvider(new A())
    .withSearchProvider(new B())
    .withSearchProvider(new A())

您将在最后一行收到如下所示的错误:SearchProvider already exists。 我想稍微改进一下:SearchProvider A already exists 这是我开始挣扎的地方:

type CombineExclusive<Host, Key extends string, Provider> = <
  P extends Host extends Has<Key, P> ? `${Key} ${P} already exists` : Provider
>(
  provider: P
) => Host extends Has<Key, P> ? never : Host & Has<Key, P>

我无法在模板文字中引用 P,因为它创建了循环约束。另一种方法可能是引用“原始”P,但我不知道如何,或者是否可能。 所以,我的任务是创建一个类型约束,错误消息显示参数类型 P。有什么想法吗?

Link 到 ts playground: playground

TypeScript 有时接受循环引用,有时不接受。如果你有一个 generic function type and can't get a circular reference to be accepted inside a type parameter's constraint, you can sometimes move the reference out of the constraint and into a conditionally typed 函数参数。也就是说,从这样的事情:

function orig<T extends F<T>>(param: T) { } // error, circular constraint

像这样:

function fixed<T>(param: T extends F<T> ? T : F<T>) { } // okay

写成T extends F<T> ? T : F<T>有点奇怪,但一般来说编译器会推断Tparam的类型。因此,如果 T extends F<T> 符合要求,函数将看起来像 function fixed<T>(param: T) {} 并且不会有错误。另一方面,如果 T extends F<T> 不满足,则函数看起来像 function fixed<T>(param: F<T>) 并且由于 paramT 而不是 F<T> 类型,你'当您违反通用约束时,您会得到一个与您得到的错误非常相似的错误。


在您的示例中,这可以更改为:

type CombineExclusive<Host, Key extends string, Provider> = <
  P extends Provider
  >(provider: P extends (Host extends Has<Key, P> ? never : unknown) ? P :
    `${Key} of type '${Extract<P, { type: string }>['type']}' already exists`
) => Host extends Has<Key, P> ? never : Host & Has<Key, P>

稍微改了一下,还是一样的效果;如果 Host extends Has<Key, P> 为真,则 this 变为 P extends unknown ? P : `...` 变为 P 并且调用将成功。如果 Host extends Has<Key, P> 为 false,则这变为 P extends never ? P : `...` 变为 `...` 并且调用将失败,模板文字作为错误消息的一部分。

另请注意,您不能通过 `${P}`P 序列化为字符串,因为 P 不是 string/number/boolean/bigint(根据ms/TS#40336的要求)。所以我采用 P,它应该具有 stringtype 属性,并将其放入消息中。

让我们看看实际效果:

const liveApplication = (app: Application) =>
  app
    .withSearchProvider(new A())
    .withSearchProvider(new B())
    .withSearchProvider(new A()) // error 
// -------------------> ~~~~~~~
// Argument of type 'A' is not assignable to parameter of type 
// '"SearchProvider of type 'a' already exists"'.(2345)

看起来不错!

Playground link to code