打字稿动态泛型参数

时间:2021-03-17 17:22:07

标签: typescript typescript-generics

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

在本例中,我有主函数 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 

1 个答案:

答案 0 :(得分: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 信息只有在实际传递参数时才可用,而这不能在类型中发生。因此,结果是 EXTRA1 | EXTRA2 | EXTRA3 等等,对于每个使用过的 Mod

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 & EXTRA2

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