typescript 为什么ConditionalType解析为与预期不同的类型?

ajsxfq5m  于 2023-01-10  发布在  TypeScript
关注(0)|答案(3)|浏览(157)
    • bounty将于明天过期**。此问题的答案可获得+100的声誉奖励。Krzysztof Kaczyński正在寻找来自声誉良好的来源的答案:我不仅对如何解决这个问题感兴趣,而且还想了解TS为什么要以这种方式解决这个条件类型

我想知道如果我添加了一个应该保证期望类型的约束,为什么TypeScript会将条件类型解析为never

示例

假设我们已经编写了这样的代码

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = T extends PrimitiveDataType
  ? (v: T) => void
  : never;

abstract class AbstractClass<T> {
  abstract value: T;
  protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}

class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> {
  value: T;

  constructor(value: T) {
    super();
    this.value = value;
  }

  someMethod() {
    for (const someFn of this.conditionalFunctions.keys()) {
      someFn(this.value);
    }
  }
}

TSPlayground链接

在上面的代码中,我创建了一个PrimitiveDataType,它是JavaScript中所有原始数据类型的并集。
然后,我创建了一个ConditionalType<T>,只有当TPrimitiveDataType之一时,它才会解析为某个回调。
然后我创建了一个抽象泛型类,它有一个类型(ConditionalType<T>)依赖于该类泛型值的字段。
最后,我创建了一个SomeClass,它扩展了AbstractClass<T>,并添加了泛型参数T必须扩展PrimitiveDataType的约束,我得到了这个错误:

TS2345: Argument of type 'PrimitiveDataType' is not assignable to parameter of type 'never'.   
Type 'undefined' is not assignable to type 'never'.

结论

我想如果在SomeClassT中有一个约束,它必须是PrimitiveDataType上的一个,那么TypeScript将解析conditionalFunctions字段为Map<(v: T) => void, number | undefined>类型。令我惊讶的是,TypeScript将此类型解析为Map<(v: never) => void, number | undefined>,这对我来说是不清楚的,我不知道哪里是错误的方式,我该怎么想?
你能给我解释一下为什么会这样吗?或者可能是TypeScript编译器的bug?

观察

如果我只留下一个,键入PrimitiveDataType,那么一切正常,但对于多个,我得到一个错误

编辑1

如果我的条件类型有两个以上可能的返回类型,那么分布式[T]就不起作用,并给出这样的错误:

Expected 2 arguments, but got 1.

看起来TS现在把这个类型解析成了Array的Map类型,我现在完全不明白

    • 示例**
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t: number) => void
  : never;

abstract class AbstractClass<T> {
  abstract value: T;
  protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}

class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> {
  value: T;

  constructor(value: T) {
    super();
    this.value = value;
  }

  someMethod() {
    for (const someFn of this.conditionalFunctions.keys()) {
      someFn(this.value);
    }
  }
}

TSPlayground-编辑1

在我看来,这应该和这个例子一样:

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t: number) => void
  : never;

const x: PrimitiveDataType = 12;
const y: ConditionalType<typeof x> = (param: number) => undefined;

TSPlayground-变量示例

没有问题,ConditionalType<typeof x>被解决为(v: number) => void,所以我所期望的。

30byixjq

30byixjq1#

泛型类型ConditionalType<T>在给定联合时生成一个分布式类型。
例如,给定并集string | number时,将生成以下类型

type Foo = ConditionalType<string | number>
// ((v: string) => void) | ((v: number) => void)

请将条件的任意一侧括在方括号中以避免出现这种情况

type ConditionalType<T> = [T] extends [PrimitiveDataType]
  ? (v: T) => void
  : never;

上面的代码现在生成了下面的非分布式类型,它应该可以为抽象类工作

type Foo = ConditionalType<string | number>
// (v: string | number) => void

Playground

2admgd59

2admgd592#

我在 typescript 方面相当初级,我已经用了差不多一年半了,但我仍然会尽力帮助你,给予你一个可能的解释,解释你遇到的错误(至少我认为是解释),以及修复它的可能方法。
/* ===============解释============ /
一个原因,也是对我来说最有意义的原因,可能是类型收缩不够充分。为什么?
在您提供的代码中,您将类型ConditionalType<T>定义为一个函数,当TPrimitiveDataType时,该函数返回void;当T不是PrimitiveDataType时,该函数返回never。然而,您使用此函数类型的方式可能不是最好的。此函数类型的用途是T类型的narrow the type,应该作为type guard使用。但是,当您声明类SomeClass时,T类型扩展原始PrimitiveDataType而不是narrowed类型,因此在for循环中,您将检查原始类型PrimitiveDataType是否是narrowed类型版本的有效key(正如您在AbstractClass中声明的那样),但是never并不存在于PrimitiveDataType上,这就是为什么你应该在类SomeClass声明中扩展T,如我在下面提供的第二个修复所示。这样的话,T也会默认包含never,所以value类型会是narrowed版本类型,也就是ConditionalType<PrimitiveDataType>,而不是PrimitiveDataType
我希望你明白我的意思(我的英语仍然不是很好哈哈)。
/
===============修复============= */
我将提供两种方法来修复这个问题,第二种是类型安全性最高的方法。
我想你可以用两种方法解决这个问题:
第一种方法是将PrimitiveDataType定义为始终是函数类型:

type ConditionalType<T> = (v: T) => void;

当然,在上面的例子中,可能ConditionalType不再是正确的名称,请找到一个更好的名称。链接到上面描述的例子的typescriptPlayground-〉TypescriptPlayground
/* =============或================ */
或者你可以使用另一种方法,其中类型参数T被约束为ConditionalType<PrimitiveDataType>类型的子类型。在这种情况下,T扩展了ConditionalType<PrimitiveDataType>,这意味着T必须是function type,它接受PrimitiveDataType类型的参数并返回void

type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;

type ConditionalType<T> = T extends PrimitiveDataType
  ? (v: T) => void
  : never;

abstract class AbstractClass<T> {
  abstract value: T;
  protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}

class SomeClass<T extends ConditionalType<PrimitiveDataType>> extends AbstractClass<T> {
  value: T;

  constructor(value: T) {
    super();
    this.value = value;
  }

  someMethod() {
    for (const someFn of this.conditionalFunctions.keys()) {
        someFn(this.value);        
    }
  }
}

链接到第二种方法的 typescript Playground-〉 typescript Playground
/* ==============编辑1 =============== /
EDIT 1怎么样
当你尝试使用多个参数时遇到的错误的解释几乎是一样的,它总是与类型收缩有关,但是修复非常非常简单,你只是忘了把第二个参数设为可选的。
/
==============固定编辑1 ============= */

type ConditionalType<T> = T extends PrimitiveDataType
  ? (v: T) => void
  : T extends Array<unknown>
  ? (v: T, t?: number) => void
  : never;

这里有完整的代码修复编辑1 -〉 typescript 操场 typescript 操场
请让我知道这是否有帮助

0aydgbwb

0aydgbwb3#

如上所述,分布式类型会产生问题,解决方法是将extends子句的范围缩小到所需的函数类型。
首先,看一个工作的例子(基于你的原始代码)。我在下面添加了两个例子来展示一个工作的(字符串)和不工作的(对象)例子。顺便说一句,这是一个熟悉info指令的好机会,只需在一行中输入'// ^?',ts pladren就会给你一些提示信息。
因此,修复的关键是检查所需的函数(而不仅仅是类型):

type ConditionalType<T> = ((v: PrimitiveDataType) => void) extends ((v: T) => void)
  ? (v: T) => void
  : never;

读作:
雌兔
接受PrimitiveDataType参数并返回void的函数
延伸
一个接受我的类型的参数(泛型的T)并返回void的函数

如果是,则返回所需的函数类型,否则,返回never。

相关问题