类型的Typescript递归子集

时间:2019-03-01 16:51:00

标签: typescript

在Typescript中是否可以创建类似于此类型的子集?

type Schema = {
  user: {
    name: string;
    age: number;
    profile: {
      isCool?: boolean;
    };
  };
};

const wantedSubset = {
  user: {
    name: true,
    profile: {
      isCool: true
    }
  }
};

type ExpectedType = {
  user: {
    name: string;
    profile: {
      isCool?: boolean;
    };
  };
};

const result: ExpectedType = desiredCode(wantedSubset);

在不清楚的情况下,我想拥有desiredCode函数,给定对象wantedSubset将返回除键入为declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;的递归使用之外的任何内容

我想将此Pick递归地应用于Schema,但是我不知道如何递归地“引用” Schema类型。

我尝试过这样的事情:

function gql<Q, S, QK extends keyof Q, SK extends keyof S, K extends SK & QK>(
  query: Q | true,
  schema: S
) {
  if (query === true) return (query as unknown) as S;
  const keys = Object.keys(query) as K[];
  const result = {} as Pick<S, K>;
  keys.forEach(k => {
    result[k] = gql(query[k], schema[k]);
  });

  return result;
}

const result = gql(wantedSubset, wantedSubset as unknown as Schema)

但是它不能按照我想要的方式工作。它只是返回Pick的结果,而不是递归地应用它。

基本上,问题在于如何动态地构建对象,以便打字稿能够推断出返回值。

https://www.typescriptlang.org/play/#src=type%20Schema%20%3D%20%7B%0D%0A%20%20user%3A%20%7B%0D%0A%20%20%20%20name%3A%20string%3B%0D%0A%20%20%20%20age%3A%20number%3B%0D%0A%20%20%20%20profile%3A%20%7B%0D%0A%20%20%20%20%20%20isCool%3F%3A%20boolean%3B%0D%0A%20%20%20%20%7D%3B%0D%0A%20%20%7D%2C%0D%0A%20%20foo%3A%20%7B%0D%0A%20%20%20%20bar%3A%20string%3B%0D%0A%20%20%7D%0D%0A%7D%3B%0D%0A%0D%0Aconst%20wantedSubset%20%3D%20%7B%0D%0A%20%20user%3A%20%7B%0D%0A%20%20%20%20name%3A%20true%2C%0D%0A%20%20%20%20profile%3A%20%7B%0D%0A%20%20%20%20%20%20isCool%3A%20true%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%7D%3B%0D%0A%0D%0A%0D%0Afunction%20gql%3CQ%2C%20S%2C%20QK%20extends%20keyof%20Q%2C%20SK%20extends%20keyof%20S%2C%20K%20extends%20SK%20%26%20QK%3E(%0D%0A%20%20query%3A%20Q%20%7C%20true%2C%0D%0A%20%20schema%3A%20S%0D%0A)%20%7B%0D%0A%20%20if%20(query%20%3D%3D%3D%20true)%20return%20(query%20as%20unknown)%20as%20S%3B%0D%0A%20%20const%20keys%20%3D%20Object.keys(query)%20as%20K%5B%5D%3B%0D%0A%20%20const%20result%20%3D%20%7B%7D%20as%20Pick%3CS%2C%20K%3E%3B%0D%0A%20%20keys.forEach(k%20%3D%3E%20%7B%0D%0A%20%20%20%20result%5Bk%5D%20%3D%20gql(query%5Bk%5D%2C%20schema%5Bk%5D)%3B%0D%0A%20%20%7D)%3B%0D%0A%0D%0A%20%20return%20result%3B%0D%0A%7D%0D%0A%0D%0Aconst%20result%20%3D%20gql(wantedSubset%2C%20wantedSubset%20as%20unknown%20as%20Schema)%0D%0A%0D%0Aresult.user.age%20%2F%2F%20should%20be%20an%20error!%0D%0Aresult.user.name%20%2F%2F%20works%20OK%0D%0Aresult.user.profile.isCool%20%2F%2F%20works%20OK%0D%0Aresult.foo%20%2F%2F%20works%20like%20expected

1 个答案:

答案 0 :(得分:2)

因此,我认为您希望wantedSubset符合一种类型,其中每个属性均为true或本身就是该类型的对象。我们需要做一些工作才能使TypeScript推断出这样一种类型,其中将值true视为类型true而不是boolean(至少直到TS3.4出来并给我们const contexts):

type WantedSubset = { [k: string]: true | WantedSubset };
const asWantedSubset = <RP extends WantedSubset>(wantedSubset: RP) => wantedSubset;

const wantedSubset = asWantedSubset({
  user: {
    name: true,
    profile: {
      isCool: true
    }
  }
}); // no error, so that type works.

现在出现RecursivePick类型的朋友。首先,给定类型T,您需要一个符合它的WantedSubset类型。我称其为RecusivePicker<T>

type RecursivePicker<T> = { 
  [K in keyof T]?: T[K] extends object | undefined ? RecursivePicker<T[K]> : true 
}

因此,如果TSchema,那么RecursivePicker<Schema>会给您:

type RPSchema = RecursivePicker<Schema>
// type RPSchema = {
//  user?: {
//    age?: true;
//    name?: true;
//    profile?: {
//      isCool?: true;
//    }
//  }
// } 

这可能是对wantedSubset类型的约束,您可以从Schema类型中进行选择。

这是RecursivePick的所有递归映射条件恐怖荣耀:

type RecursivePick<T, KO extends RecursivePicker<T>> =
  Pick<{ [K in keyof T]: K extends keyof KO ?
    KO[K] extends true ? T[K] :
    KO[K] extends infer KOK ?
    KOK extends RecursivePicker<T[K]> ? RecursivePick<T[K], KOK> :
    never : never : never }, keyof T & keyof KO>;

基本上,它遍历了T的属性并检查了KO的相应属性。如果属性为true,则返回的属性T不变。如果属性本身是一袋属性,则递归向下。如果该属性不存在,它将返回never。而且整个过程都是Pick的,因此只有出现在TKO中的键才会出现在最终输出中(这是在摆弄所有相关的{{ 3}}是同态的,这意味着可选属性保持可选)。

让我们验证一下它是否有效:

type ExpectedType = RecursivePick<Schema, typeof wantedSubset>;

您可以逐步解决,但让编译器进行验证:

type ExpectedTypeManual = {
  user: {
    name: string;
    profile: {
      isCool?: boolean;
    };
  };
};

type MutuallyExtends<T extends U, U extends V, V=T> = true

// if no error in the next line, then ExpectedType and ExpectedTypeManual are 
//  structurally the same
type NoErrorHere = MutuallyExtends<ExpectedType, ExpectedTypeManual>

一切正常。您的函数将被输入如下内容:

declare function gql<Q extends RecursivePicker<S>, S>(
  query: Q | true,
  schema: S
): RecursivePick<S, Q>;

const result = gql(wantedSubset, null! as Schema); // looks good

要获得无错误编译的函数实现,可能需要mapped types,因为众所周知,条件类型很难推断:

function gql<Q extends RecursivePicker<S>, S>(
  query: Q | true,
  schema: S
): RecursivePick<S, Q>;
function gql(query: any, schema: object): object {
  return {}; // something better here
}

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