【问题标题】:Typescript dynamic generic arguments打字稿动态泛型参数
【发布时间】:2021-03-18 16:38:00
【问题描述】:

我想要一个返回对象的函数。该函数还可以将n个函数作为args,并在返回之前使用它们来修改对象。我的问题是在进行更改后我没有得到正确的打字稿类型。

在这个例子中,我有主函数getUser 和两个修饰函数modStatusmodAge。当我使用两个修饰符调用getUser 时,我想将UserModStatus & UserModAge & User 作为返回类型

type User = {
  name: string;
};

type UserModAge = {
  age: number;
};

type UserModStatus = {
  status: "single" | "married";
};

type Mod<EXTRA> = <BASE>(user: BASE) => EXTRA & BASE;

const getUser = <T>(...mods: Mod<T>[]) => {
  const user: User = {
    name: "John",
  };

  return mods.reduce((acc, mod) => mod(acc), user);
};

const modAge: Mod<UserModAge> = (user) => {
  return { ...user, age: 21 };
};

const modStatus: Mod<UserModStatus> = (user) => {
  return { ...user, status: "married" };
};

const res = getUser(modStatus, modAge); // const res: User

但是返回类型只是初始的“用户”类型。

如果我使用静态参数,它可以正常工作

const getUser = <T, V>(mod1: Mod<T>, mod2: Mod<V>) => {
  const user: User = {
    name: "John",
  };

  return mod1(mod2(user));
};

const res = getUser(modStatus, modAge); // const res: UserModStatus & UserModAge & User

但我需要它们是动态的。有没有办法做到这一点?

期望的结果是在应用不同的修饰符时得到正确的类型

const res = getUser();
// const res: User 
const res = getUser(modAge);
// const res: UserModAge & User 
const res = getUser(modStatus);
// const res: UserModStatus & User 
const res = getUser(modStatus, modAge);
// const res: UserModStatus & UserModAge & User 

【问题讨论】:

标签: typescript typescript-generics


【解决方案1】:

是的,你可以做到这一点,但它需要一些高级类型(或者更确切地说,我需要它们)。我只会展示我添加的内容,但完整的示例可以在playground 中查看:

// nothing new added to the next line, but im referring often to Mod and the EXTRA generic.
type Mod<EXTRA> = <BASE>(user: BASE) => EXTRA & BASE;
// Converts Mod<any>[] into EXTRA1 | EXTRA2 | EXTRA3...
type UnionOfReturnTypes<U extends Mod<any>[]> = ReturnType<U[number]>

// Converts EXTRA1 | EXTRA2 | EXTRA3 into EXTRA1 & EXTRA2 & EXTRA3
type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

// This takes a BASE (User in our case) and intersects it with EXTRA1 & EXTRA2 & EXTRA3.
// It also combines the types UnionOfReturnTypes and UnionToIntersection
type ModifiedResult<BASE, MODS extends Mod<any>[]>  = BASE & UnionToIntersection<UnionOfReturnTypes<MODS>>;

const getUser = <T extends Mod<any>[]>(...mods: T) => {
  const user: User = {
    name: "John",
  };

  return mods.reduce((acc, mod) => mod(acc), user) as ModifiedResult<User, T>;
};


const res = getUser(modStatus, modAge); // const res: User
// No compiler errors and intellisense
res.age; 
res.name;
res.status;

说明:

UnionOfReturnTypes:此类型使用ReturnTypeindexing 组合来自Mod 函数的所有返回类型。但它只能提取EXTRA 类型,因为BASE 信息仅在实际传递参数时可用,而这在类型中不会发生。因此,对于每个使用的 Mod,结果都是 EXTRA1 | EXTRA2 | EXTRA3 等等。

UnionToIntersection 这段代码来自answer of jcalz,我们需要它来将我们的并集转换为交集。 UnionOfReturnTypes 返回的联合对我们没有多大帮助,因为我们想要的 user 结果不是:

type finalUser = {name: string} | {age: number} | {status: "married" | "single"}

但结果如下:

type finalUser = {
  name: string,
  age: number,
  status: "married" | "single"
};

而这正是UnionToIntersection 对我们的作用,它转换为EXTRA1 | EXTRA2 -> EXTRA1 &amp; EXTRA2

ModifiedResult 这是结合了UnionToIntersectionUnionOfReturnTypes 的类型,还添加了我们无法从UnionOfReturnTypes 中提取的基础。这最终确定了解决方案,现在让我们动态地将 mod 添加到 getUser() 并获取返回的完整类型。

【讨论】:

  • 如果有兴趣,我也可以尝试用简单的语言解释一下 UnionToIntersection 的实际工作原理。不过这可能是一个挑战 =)
猜你喜欢
  • 2018-05-02
  • 2020-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-22
  • 2023-02-22
  • 2015-11-27
  • 2022-11-11
相关资源
最近更新 更多