过滤对象属性(键),保留类型信息。

huangapple go评论52阅读模式
英文:

Filter object properties (keys) keeping typings

问题

我正在尝试构建一个函数来过滤(包括 | 排除)对象属性。JS函数工作正常,但我无法弄清楚如何正确地为其添加类型。到目前为止,这是我的尝试:

// 过滤对象键
export const filterObjKeys = <T extends {}, K extends keyof T>(
  obj: T,
  includeKeys: K[] = [],
  excludeKeys: K[] = [],
): T => {
  return Object.fromEntries(
    Object.entries(obj).filter(([k, v]) =>
      includeKeys?.length > 0 ? includeKeys.includes(k) : !excludeKeys.includes(k),
    ),
  ) as Omit<T, typeof excludeKeys>;
};

这是我的期望结果(它可以工作,但结果上的类型不正确,它们保持与完整接口相同):

interface Student {
  firstName: string;
  lastName: string;
  email: string;
  class: number;
}

const student: Student = {
  firstName: 'John',
  lastName: 'Doe',
  email: 'a@b.com',
  class: 3,
};

const filteredStudentInc = filterObjKeys(student, ['firstName', 'lastName']);
// {firstName: 'John', lastName: 'Doe'}

const filteredStudentExc = filterObjKeys(student, [], ['email'])
// {firstName: 'John', lastName: 'Doe', class: 3}

请注意,我纠正了接口名称中的拼写错误,并将 "firtsName" 更正为 "firstName"。

英文:

I'm trying to build a function to filter (include | exclude) an object properties.
JS Function is working fine but I can't figure out how to type it correctly.
Here's what I've attempted so far:

// filter object keys
export const filterObjKeys = &lt;T extends {}, K extends keyof T&gt;(
  obj: T,
  includeKeys: K[] = [],
  excludeKeys: K[] = [],
): T =&gt; {
  return Object.fromEntries(
    Object.entries(obj).filter(([k, v]) =&gt;
      includeKeys?.length &gt; 0 ? includeKeys.includes(k) : !excludeKeys.includes(k),
    ),
  ) as Omit&lt;T, typeof excludeKeys&gt;;
};

This is my desired result (it works but types on results are wrong, they remain the same of the complete interface):

interface Student {
  firtsName: string;
  lastName: string;
  email: string;
  class: number;
}

const student: Student = {
  firtsName: &#39;John&#39;,
  lastName: &#39;Doe&#39;,
  email: &#39;a@b.com&#39;,
  class: 3,
};

const filteredStudentInc = filterObjKeys(student, [&#39;firtsName&#39;, &#39;lastName&#39;]);
// {firtsName: &#39;John&#39;, lastName: &#39;Doe&#39;}

const filteredStudentExc = filterObjKeys(student, [], [&#39;email&#39;])
// {firtsName: &#39;John&#39;, lastName: &#39;Doe&#39;, class: 3}

答案1

得分: 2

首先,让我们更新我们的约束。
T extends {} 并不意味着 T 是非基本类型。正确的选项应该是 object

// true
type Case1 = number extends {} ? true : false
// false
type Case2 = number extends object ? true : false

对于键值,你可能只需要字符串键,因此让我们仅提取字符串键,可以通过交集 keyof T & string 或使用 Extract Extract<keyof T, string> 来实现:

现在,让我们实现 pickObjKeys

export const pickObjKeys = <T extends object, K extends keyof T & string>(
  obj: T,
  keys: K[],
): Pick<T, K> => {
  return Object.fromEntries(keys.map((key) => [key, obj[key]])) as Pick<T, K>;
};

我简化了实现逻辑,现在不再使用 Object.entries,而是使用 Array.prototype.map 来生成条目。请注意,不幸的是,无法使整个实现完全类型安全,因此我们需要使用断言来消除错误。要实际选择字段,我们使用了内置的 Pick 实用程序类型。

测试:

// const filteredStudentInc: Pick<Student, "firtsName" | "lastName">
const filteredStudentInc = pickObjKeys(student, ['firtsName', 'lastName']);

看起来正确,但如果我们想要查看类型的确切外观,我们将需要一些实用程序,使结果类型更易读:

Prettify - 接受一个类型并返回其简化版本,以提高可读性。将接口转换为类型,简化交集

type Prettify<T> = T extends infer R
  ? {
      [K in keyof R]: R[K];
    }
  : never;

Prettify 起作用是因为我们重新声明了类型并重新映射了其字段,从而得到了更具文本性的类型。

测试:

const pickObjKeys = <T extends object, K extends keyof T & string>(
  obj: T,
  keys: K[],
): Prettify<Pick<T, K>> => {
  return Object.fromEntries(keys.map((key) => [key, obj[key]])) as Prettify<
    Pick<T, K>
  >;
};

// const filteredStudentInc: {
//   firtsName: string;
//   lastName: string;
// }
const filteredStudentInc = pickObjKeys(student, ['firtsName', 'lastName']);

现在,让我们转向 excludeObjKeys

const excludeObjKeys = <T extends object, K extends keyof T & string>(
  obj: T,
  keys: K[],
): Prettify<Omit<T, K>> => {
  return Object.fromEntries(
    Object.keys(obj).reduce<[string, unknown][]>((acc, key) => {
      if (!keys.includes(key as K)) acc.push([key, obj[key as K]]);

      return acc;
    }, []),
  ) as Prettify<Omit<T, K>>;
};

这里的逻辑有点复杂,但我尽量进行了优化。我们使用 Object.keys 获取键,并使用 Array.prototype.reduce() 既生成条目又过滤掉排除的属性。要从类型中删除属性,我们需要内置的 Omit 实用程序类型。

测试:

// const filteredStudentExc: {
//   firtsName: string;
//   lastName: string;
// }
const filteredStudentExc = excludeObjKeys(student, ['email', 'class']);

[Playground链接](https://www.typescriptlang.org/play?target=6&ts=5.0.4#code/C4TwDgpgBACgThYwCWAzEAeAKgPigXiiyggA9gIA7AEwGcplLUI4oAlAKCigH4oBvLt2EBtANINKUANYQQAe1TsAugC5245QG4h3AL5D1lCADcWOrgGN5lWsChhkl6QHkARgCsxc+oWwlyKjooeU8IS2AAGigJMgoaelkFJWIAMig7OEYAcxwACiFQj3UsSKEk2nUxEWUygEp1eEQUdAwYJ2lsaLEcPHw8QW4EYABXOCl3D3DgADpUOHkAWwBRSmAsiFo8ipnFgEMwPO25OoI8ESTooou5ZWU60736JqQ0TF1YDq6YnCEcHT0Fig1ls9jIlgANiNqBBJt4QL4oP44kF6EVpt0AvFgklFEQoOlMjl8oVPCUytwKlUavVGsMWpgXItkMBvj0+gMhMMxhMwhE5gsVmsNlsPpNpjMKnkinUZghqCNLBAMCIiZRstERpRpJR5AB3SjKGr5PJ7SyWaJJU79AQfbhoKB5ACEO0YkOhm2OICgTxiDx95pmYBGtAAFnkbiArp5Iz76GI7nUgcIhogeQHLDoU3pojU6hSoI9nvS3hgmSy2b0ARZGBQ4KgzdAAMqjGFrW3cVDIODAWgAOT2iwg6jV2SzUAhT2AA6HI-WOXHEH2yAhc6y6vHkKelSglBGizc5g4Bg4ILsGVbVGA6hbHvbhEGUC7Pf7g+HUAA5AApeShygfgtJzsGd3w-AAReQIAAoQlz2Fd1A-PYAAE3BmaxFmg7gt1

英文:

First of all, let's update our constraints.
T extends {} doesn't mean that T is a non-primitive type. The correct option would be object:

// true
type Case1 = number extends {} ? true : false
// false
type Case2 = number extends object ? true : false

For the keys probably you will need only string keys, thus let's extract only string keys by either intersecting keyof T &amp; string or using Extract Extract&lt;keyof T, string&gt;

Now, let's implement the pickObjKeys:

export const pickObjKeys = &lt;T extends object, K extends keyof T &amp; string&gt;(
  obj: T,
  keys: K[],
): Pick&lt;T, K&gt; =&gt; {
  return Object.fromEntries(keys.map((key) =&gt; [key, obj[key]])) as Pick&lt;T, K&gt;;
};

I've simplified the implementation's logic, now instead of Object.entries we generate entries using Array.prototype.map over the passed keys. Note that unfortunately, there will be no way to make the whole implementation fully type-safe, thus we will need to use assertion to get rid of the error. To actually pick the fields we use the built-in Pick utility type.

Testing:

// const filteredStudentInc: Pick&lt;Student, &quot;firtsName&quot; | &quot;lastName&quot;&gt;
const filteredStudentInc = pickObjKeys(student, [&#39;firtsName&#39;, &#39;lastName&#39;]);

It looks correct, but if we want to see how the type exactly looks we will need some utility that will make the result types more readable:

Prettify - Accepts a type and returns its simplified version for better readability. Transforms interface to type, simplifies intersections

type Prettify&lt;T&gt; = T extends infer R
  ? {
      [K in keyof R]: R[K];
    }
  : never;

Prettify works because we redeclare the type and remap its fields, which results in more literal types.

Testing:

const pickObjKeys = &lt;T extends object, K extends keyof T &amp; string&gt;(
  obj: T,
  keys: K[],
): Prettify&lt;Pick&lt;T, K&gt;&gt; =&gt; {
  return Object.fromEntries(keys.map((key) =&gt; [key, obj[key]])) as Prettify&lt;
    Pick&lt;T, K&gt;
  &gt;;
};

// const filteredStudentInc: {
//   firtsName: string;
//   lastName: string;
// }
const filteredStudentInc = pickObjKeys(student, [&#39;firtsName&#39;, &#39;lastName&#39;]);

Now, let's jump to the excludeObjKeys:

const excludeObjKeys = &lt;T extends object, K extends keyof T &amp; string&gt;(
  obj: T,
  keys: K[],
): Prettify&lt;Omit&lt;T, K&gt;&gt; =&gt; {
  return Object.fromEntries(
    Object.keys(obj).reduce&lt;[string, unknown][]&gt;((acc, key) =&gt; {
      if (!keys.includes(key as K)) acc.push([key, obj[key as K]]);

      return acc;
    }, []),
  ) as Prettify&lt;Omit&lt;T, K&gt;&gt;;
};

The logic here is a bit more complex, however, I've tried to optimize it as I could. We get the keys using Object.keys and using Array.prototype.reduce() we both generate entries and filter out excluded properties. And to remove the properties from the type we need the built-in Omit utility type.

Testing:

// const filteredStudentExc: {
//   firtsName: string;
//   lastName: string;
// }
const filteredStudentExc = excludeObjKeys(student, [&#39;email&#39;, &#39;class&#39;]);

playground

huangapple
  • 本文由 发表于 2023年7月23日 20:16:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76748186.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定