联合函数类型的推断

Inference of union function types

假设我有类似的架构:

type GetDog = () => { animal: string; bark: boolean };
const getDog: GetDog = () => ({ animal: 'dog', bark: true });

type GetCat = () => { animal: string; meow: boolean };
const getCat: GetCat = () => ({ animal: 'cat', meow: true });

type AnimalFactory =
  | ((callback: GetDog) => ReturnType<typeof getDog>)
  | ((callback: GetCat) => ReturnType<typeof getCat>);

const calmAnimalFactory: AnimalFactory = (callback) => {
  // Some fancy stuff
  return callback();
};

calmAnimalFactory作为参数应该接受getDog函数或getCat函数,然后是returns对应的值。问题是我猜类型推断并没有像我想象的那样工作。 calmAnimalFactory 内的回调不会推断 GetDog | GetCat 的类型,而是 any。我认为 Typescript 应该以 calmAnimalFactory(getDog) 应键入为

的方式确定 calmAnimalFactory 的类型
((callback: GetDog) => ReturnType<typeof getDog>)

calmAnimalFactory(getCat)应该输入为

((callback: GetCat) => ReturnType<typeof getCat>)

我希望它可以实现,但我不知道为什么它不能那样工作。

您可以使用泛型对此进行正确的输入。我们说 calmAnimalFactory 依赖于表示回调类型的泛型 T。此 T 必须是一个接受零参数和 return 任何参数的函数。

现在我们可以说 calmAnimalFactorycallback 参数是 T 并且函数的 return 类型与 return 相同回调类型。

const calmAnimalFactory = <T extends () => any>(callback: T): ReturnType<T> => {
  // Some fancy stuff
  return callback();
};

const dog = calmAnimalFactory(getDog); // type: { animal: string; bark: boolean; }
const cat = calmAnimalFactory(getCat); // type { animal: string; meow: boolean; }

同一想法的一个略有不同的版本是让通用 T 指代所创建动物的类型。这使得要求回调的 return 必须适合某些基础 Animal 接口变得更容易。

interface Animal {
  animal: string;
}

const calmAnimalFactory = <T extends Animal>(callback: () => T): T => {
  // Some fancy stuff
  return callback();
};

catdog 的类型仍然相同。

Typescript Playground Link

这里发生了一些事情。首先,我认为如果我必须一直输入 typeof getXXXReturnType<typeof getXXX>,我会发疯,所以我将定义新的 DogCat 接口,对应什么getDog()getCat() return,并专门使用它们:

interface Dog extends ReturnType<GetDog> { };
interface Cat extends ReturnType<GetCat> { }

您可以并且可能应该首先定义这些 DogCat 类型,然后根据它们编写 getCat()getDog()

interface Dog {
  animal: string;
  bark: boolean;
}

type GetDog = () => Dog;
const getDog: GetDog = () => ({ animal: 'dog', bark: true });

interface Cat {
  animal: string;
  meow: boolean;
}
type GetCat = () => Cat;
const getCat: GetCat = () => ({ animal: 'cat', meow: true });

但任何一种方法都可以。


下一步,如果你想要一个AnimalFactory到return一个Cat,当你传递一个GetCat参数时, 当你向它传递一个 GetDog 参数时,你希望它 return 一个 Dog,然后你想使用一个 intersection (&) and not a union (|)。如果你使用工会,你是说动物工厂将 变成 GetCat 变成 Cat 它将把 GetDog 变成 Dog,但不一定是两者。所以我们要交集在这里:

type AnimalFactory =
  ((callback: GetDog) => Dog)
  & ((callback: GetCat) => Cat);

接下来,编译器无法很好地推断未注释和未断言的函数实现是否符合这样的签名交集。这样的推理等同于能够正确地进行类型检查 overloaded functions, which the compiler simply cannot do (see this comment in microsoft/TypeScript#35338 例如)。

因此,如果您只是尝试根据上下文键入 callback 参数,编译器将失败,并最终隐式使用 any,如您所见。


最好使用一个 generic 函数实现,它声称能够将 () => T 类型的 callback 用于泛型 T extends Cat | Dog,然后生成一个T。这是编译器可以类型检查的东西:

const calmAnimalFactory: AnimalFactory =
  <T extends Cat | Dog>(callback: () => T) => {
    return callback();
  };

编译器能够验证 <T extends Cat | Dog>(callback: () => T) => T 类型的函数是否可分配给 AnimalFactory 的两个调用签名,因此编译没有错误。


事实上,根据您的用例,您可能希望通过消除对 CatDog 的依赖,使 AnimalFactory 尽可能通用,并提出一些基本类型,如 Animal,例如:

interface Animal {
  animal: string;
}
type UniversalAnimalFactory = <T extends Animal>(callback: () => T) => T;
const universalAnimalFactory: UniversalAnimalFactory = callback => callback();

UniversalAnimalFactory 可以做 AnimalFactory 可以做的任何事情(GetCat 进,Cat 出;GetDog 进,Dog out) 但它还可以为您尚未想到的动物做任何其他事情:

const getFish = () => ({ animal: "fish", swim: true });
universalAnimalFactory(getFish).swim // works

Playground link to code