TypeScript复杂映射类型提取泛型

5

TypeScript映射类型:获取数组元素类型类似,我有一个类型,比如说:

type A = {
   Item1: Promise<string>,
   Item2: Promise<number>,
   Item3: number
}

我想从中提取出以下类型:
type A' = {
   Item1: string,
   Item2: number,
   Item3: number
}

请注意,其中一个字段不是Promise会增加复杂性。
这是否可能,还是我只是达到了TypeScript推断类型能力的极限?我尝试使用映射类型和记录类型进行调整,但我无法弄清楚。
更新:假设我想使用函数调用来完成同样的操作:
type A = {
  Item1: (string, number) => Promise<string>,
  Item2: () => Promise<number>,
  Item3: () => number
}

所需类型是:

type A' = {
  Item1: (string, number) => string,
  Item2: () => number,
  Item3: () => number
}

我认为这个情况与我所提出的第一个情况相似,但是函数返回值似乎不像我希望的那样直接明了。

2个回答

9

更新于2020年1月28日

自从TypeScript 2.8引入了条件类型,你现在可以相对容易地进行这种映射:

type A = {
  Item1: (x: string, y: number) => Promise<string>,
  Item2: () => Promise<number>,
  Item3: () => number,
  Item4: Promise<string>,
  Item5: Promise<number>,
  Item6: number
}

type UnpromiseObj<T> = { [K in keyof T]: T[K] extends Promise<infer U> ? U :
  T[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R :
  T[K]
}

type Aprime = UnpromiseObj<A>;
/* type Aprime = {
  Item1: (x: string, y: number) => string;
  Item2: () => number;
  Item3: () => number;
  Item4: string;
  Item5: number;
  Item6: number;
} */

我会保留下面的内容,以便将来后代可以了解在条件类型出现之前你必须经历的疯狂无意义的事情:

2020-01-28 结束更新


虽然没有映射条件类型类似功能的官方支持,但这并非完全不可能,只是有些困难。让我们试一试。首先,让我们设置一些类型级别的布尔逻辑:

type False = '0';
type True = '1';
type Bool = False | True;
type If<Cond extends Bool, Then, Else> = { '0': Else; '1': Then }[Cond];

所以类型If<True, Then, Else>的计算结果为Then,类型If<False, Then, Else>的计算结果为Else
第一个问题是您需要能够确定类型是否为Promise。第二个问题是,您需要能够获取Promise<T>的类型T。我将通过使用一些在运行时不存在的幻影属性来增强ObjectPromise<T>接口的声明来解决这个问题。
// if you are in a module you need to surround 
// the following section with "declare global {}"
interface Object {
  "**IsPromise**": False
}
interface Promise<T> {
  "**IsPromise**": True
  "**PromiseType**": T
}

那是有些棘手的部分。增强全局接口并不是一个好主意,因为它们存在于每个人的命名空间中。但它具有期望的行为:任何不是PromiseObject都有一个"**IsPromise**"属性,其类型为False,而Promise则具有一个值为True的属性。另外,Promise<T>还有一个类型为T"**PromiseType**"属性。同样,在运行时这些属性并不存在,它们只是为了帮助编译器。
现在我们可以定义Unpromise,将Promise<T>映射到T并保留任何其他类型。
type Unpromise<T extends any> = If<T['**IsPromise**'], T['**PromiseType**'], T>

还有一个名为MapUnpromise的函数,它将Unpromise映射到对象的属性上:

type MapUnpromise<T> = {
  [K in keyof T]: Unpromise<T[K]>
}

让我们看看它是否正常工作:

type A = {
   Item1: Promise<string>,
   Item2: Promise<number>,
   Item3: number
}
    
type Aprime = MapUnpromise<A>
// evaluates to { Item1: string; Item2: number; Item3: number; }

成功了!但是为了实现这一点,我们对类型进行了一些相当不愉快的操作,这可能并不值得。这取决于你的决定!

希望这能有所帮助,祝你好运!


更新1

据我所知,使用函数调用做同样的事情是不可能的。你真的需要像扩展typeof类型查询这样的东西,但这在目前的TypeScript中并不存在(截至TypeScript v2.5)。

因此,您无法从类型A中计算出APrime(请注意,A'不是有效的标识符。如果您想要,可以使用)。但是,您可以创建一个基本类型,从中可以计算出AAPrime

type False = '0';
type True = '1';
type Bool = False | True;
type If<Cond extends Bool, Then, Else> = { '0': Else; '1': Then }[Cond];
type MaybePromise<Cond extends Bool, T> = If<Cond, Promise<T>, T>

我已放弃全局增强,并添加了MaybePromise<Cond, T>,其中MaybePromise<True, T>计算为Promise<T>MaybePromise<False, T>计算为T。现在我们可以使用MaybePromise<>获取AAPrime
type ABase<Cond extends Bool> = {
  Item1: (s: string, n: number) => MaybePromise<Cond, string>,
  Item2: () => MaybePromise<Cond, number>,
  Item3: () => number
}

type A = ABase<True>;
// evaluates to { 
//   Item1: (s: string, n: number) => Promise<string>; 
//   Item2: () => Promise<number>; 
//   Item3: () => number; }


type APrime = ABase<False>;
// evaluates to { 
//   Item1: (s: string, n: number) => string; 
//   Item2: () => number; 
//   Item3: () => number; }

这样就可以了!但是我建议的重构可能不适用于您的用例。这取决于您如何首先获取A类型。哦,好吧,这是我能做的最好的。希望对您有所帮助。再次祝你好运!


太好了!我以前从未考虑过使用类型编写逻辑。有没有特定的地方可以学习更多关于这种逻辑的知识?我想要能够识别这些问题。此外,为什么 type False 不是 = false 而是字符串值 0 - Paul Sachs
啊,我明白了。不能将布尔值用作“if”类型对象的键。 - Paul Sachs
结果发现我的玩具案例并没有完全适合我的实际情况,我忘记了承诺实际上是包装在一个函数中的。我已经更新了问题。 - Paul Sachs
TypeScript并没有提供一个很好的方法来获取函数类型的返回类型。我可以建议一种更简单的重构方式,但可能不是你想要的。我会考虑一下并更新答案。 - jcalz
如果你对将逻辑和其他思想编码到类型系统中的想法感兴趣,请查找“类型级编程”。像ScalaHaskell这样的语言使用了这种方法。 TypeScript是如此新,以至于我没有看到很多关于它的类型级编程资源。 - jcalz
再次感谢您详细的回答。实际上,我应该能够使用类似这样的东西,因为我控制输入的创建位置。 - Paul Sachs

1
现在有了TypeScript 4.5的Awaited实用类型,这变得更加容易了:
type A = {
  Item1: Promise<string>;
  Item2: Promise<number>;
  Item3: number;
  Item4: (arg0: string, arg1: number) => Promise<string>;
  Item5: () => Promise<number>;
  Item6: () => number;
};

type AwaitValues<T> = {
  [K in keyof T]: T[K] extends (...args: infer P) => infer R
    ? (...args: P) => Awaited<R>
    : Awaited<T[K]>;
};

type A2 = AwaitValues<A>;

当我们将鼠标悬停在A2上时,可以看到它的类型为:

/*
type A2 = {
    Item1: string;
    Item2: number;
    Item3: number;
    Item4: (arg0: string, arg1: number) => string;
    Item5: () => number;
    Item6: () => number;
}
*/

1
你甚至可以通过推断参数和返回类型来更加简洁地完成这个任务!https://tsplay.dev/NrKjzm - Jimmy
好的!让我来编辑一下。 - ruohola

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接