【问题标题】:TypeScript Type-safe Omit FunctionTypeScript 类型安全的省略函数
【发布时间】:2019-05-26 18:00:04
【问题描述】:

我想在普通打字稿中复制 lodash 的 _.omit 函数。 omit 应该返回一个对象,其中删除了某些属性,这些属性是通过参数指定的,位于先出现的对象参数之后。

这是我最好的尝试:

function omit<T extends object, K extends keyof T>(obj: T, ...keys: K[]): {[k in Exclude<keyof T, K>]: T[k]} {
    let ret: any = {};
    let key: keyof T;
    for (key in obj) {
        if (!(keys.includes(key))) {
            ret[key] = obj[key];
        }
    }
    return ret;
}

这给了我这个错误:

Argument of type 'keyof T' is not assignable to parameter of type 'K'.
  Type 'string | number | symbol' is not assignable to type 'K'.
    Type 'string' is not assignable to type 'K'.ts(2345)
let key: keyof T

我对错误的解释是:

  1. 由于 key 是 keyof TT 是对象,因此 key 可以是 symbolnumberstring

  2. 1234563我想。所以这意味着这里有一个类型错误?

感谢任何关于为什么这不起作用以及如何使其起作用的见解!

【问题讨论】:

  • 嘿,Salami,我希望我的回答能得到反馈。
  • @NurbolAlpysbayev 感谢您的回答。我只想让其他人有机会提出一个更简单的解决方案(如果存在的话)。我不会简单地假设存在更简单的东西,但我很难完全理解您的解决方案以完全诚实:)
  • 不,没有更简单的解决方案,我保证你。因为它很简单,只是你还没有明白。你可以问我到底有什么不清楚的..

标签: javascript typescript underscore.js lodash ecmascript-2016


【解决方案1】:

如果我们将键的类型限制为字符串 [],它可以工作。 但这似乎不是一个好主意。Keys 应该是字符串 |号码 |符号[];

function omit<T, K extends string>(
  obj: T,
  ...keys: K[]
): { [k in Exclude<keyof T, K>]: T[k] } {
  let ret: any = {};
  Object.keys(obj)
    .filter((key: K) => !keys.includes(key))
    .forEach(key => {
      ret[key] = obj[key];
    });
  return ret;
}
const result = omit({ a: 1, b: 2, c: 3 }, 'a', 'c');
// The compiler inferred result as 
// {
//   b: number;
// }

【讨论】:

  • 是的,我认为这也不是一个好主意,因为这样 omit 将不适用于常规数组。
【解决方案2】:
interface Omit {
    <T extends object, K extends [...(keyof T)[]]>
    (obj: T, ...keys: K): {
        [K2 in Exclude<keyof T, K[number]>]: T[K2]
    }
}

const omit: Omit = (obj, ...keys) => {
    const ret = {} as {
        [K in keyof typeof obj]: (typeof obj)[K]
    };
    let key: keyof typeof obj;
    for (key in obj) {
        if (!(keys.includes(key))) {
            ret[key] = obj[key];
        }
    }
    return ret;
};

为方便起见,我已将大部分类型提取到界面中。

问题在于 K 被推断为 tuple,而不是 union 键。因此,我相应地更改了它的类型约束:

[...(keyof T)[]] // which can be broke down to:
keyof T // a union of keys of T
(keyof T)[] // an array containing keys of T
[...X] // a tuple that contains X (zero or more arrays like the  described one above)

然后,我们需要将元组K 转换为联合(以便将Excludekeyof T 转换为它)。它是用K[number] 完成的,我想这是不言自明的,它与T[keyof T] 创建T 值的联合相同。

Playground

【讨论】:

  • ESLint 抱怨 T extends object 但这可以替换为 T extends Record&lt;string, unknown&gt; 并且仍然有效。
  • 干杯伙伴。我不知道我这么需要这个功能?
  • 感谢sample,我稍微改写了一下 export const copyWithout = , K extends [...(keyof T)[]]>( 来源: T, ...toExclude: K ) => { return Object.keys(source).filter(key => !toExclude.includes(key)).reduce ]:T[K2]; }>((acc, curr) => { return { ...acc, [curr]: source[curr] }; }, {} as { [K in keyof typeof source]: typeof source[K] }); };
【解决方案3】:

上面 Nurbol 接受的答案可能是更多类型的版本,但这是我在 utils-min 中所做的。

它使用 typescript 内置 Omit 并且仅支持字符串键名。 (仍然需要放松 Set to Set,但其他一切似乎都很好)

export function omit<T extends object, K extends Extract<keyof T, string>>(obj: T, ...keys: K[]): Omit<T, K> {
  let ret: any = {};
  const excludeSet: Set<string> = new Set(keys); 
  // TS-NOTE: Set<K> makes the obj[key] type check fail. So, loosing typing here. 

  for (let key in obj) {
    if (!excludeSet.has(key)) {
      ret[key] = obj[key];
    }
  }
  return ret;
}

【讨论】:

    【解决方案4】:

    Object.keysfor in 将键作为字符串返回并排除符号。数字键也转换为字符串。

    您需要将数字字符串键转换为数字,否则它将返回带有字符串键的对象。

    function omit<T extends Record<string | number, T['']>,
     K extends [...(keyof T)[]]>(
        obj: T,
        ...keys: K
    ): { [P in Exclude<keyof T, K[number]>]: T[P] } {
        return (Object.keys(obj)
             .map((key) => convertToNumbers(keys, key)) as Array<keyof T>)
            .filter((key) => !keys.includes(key))
            .reduce((agg, key) => ({ ...agg, [key]: obj[key] }), {}) as {
            [P in Exclude<keyof T, K[number]>]: T[P];
        };
    }
    
    function convertToNumbers(
        keys: Array<string | number | symbol>,
        value: string | number
    ): number | string {
        if (!isNaN(Number(value)) && keys.some((v) => v === Number(value))) {
            return Number(value);
        }
    
        return value;
    }
    
    
    // without converToNumbers omit({1:1,2:'2'}, 1) will return {'1':1, '2':'2'}
    // Specifying a numeric string instead of a number will fail in Typescript
    

    要包含符号,您可以使用下面的代码。

    function omit<T, K extends [...(keyof T)[]]>(
        obj: T,
        ...keys: K
    ): { [P in Exclude<keyof T, K[number]>]: T[P] } {
        return (Object.getOwnPropertySymbols(obj) as Array<keyof T>)
            .concat(Object.keys(obj)
            .map((key) => convertToNumbers(keys, key)) as Array<keyof T>)
            .filter((key) => !keys.includes(key))
            .reduce((agg, key) => ({ ...agg, [key]: obj[key] }), {}) as {
            [P in Exclude<keyof T, K[number]>]: T[P];
        };
    }
    

    【讨论】:

    • 非常有用。只是一个小的修正,在 convertToNumbers 方法中,比较类型应该是 == 而不是 === 因为它会返回 false("2" === 2)。
    【解决方案5】:

    最简单的方法:

    export const omit = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> => {
      keys.forEach((key) => delete obj[key])
      return obj
    }
    

    作为纯函数:

    export const omit = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> => {
      const _ = { ...obj }
      keys.forEach((key) => delete _[key])
      return _
    }
    

    【讨论】:

      【解决方案6】:

      不幸的是,无法摆脱as any

      const removeProperty = <Obj, Prop extends keyof Obj>(
        obj: Obj,
        prop: Prop
      ): Omit<Obj, Prop> => {
        const { [prop]: _, ...rest } = obj;
      
        return rest;
      };
      
      export default removeProperty;
      
      
      const omit = <Obj, Prop extends keyof Obj, Props extends ReadonlyArray<Prop>>(
        obj: Obj,
        props: readonly [...Props]
      ): Omit<Obj, Props[number]> =>
        props.reduce(removeProperty, obj as any);
      

      Playground

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-02-03
        • 1970-01-01
        • 2020-09-12
        • 1970-01-01
        • 2019-07-17
        • 2021-05-25
        • 2020-07-10
        相关资源
        最近更新 更多