TypeScript类型安全的省略函式

ssgvzors  于 2022-11-18  发布在  TypeScript
关注(0)|答案(8)|浏览(109)

我想用普通的typescript复制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 T,而T是一个对象,所以key可以是symbolnumberstring
1.因为我使用了for in循环,key只能是string,但是如果我传入一个数组,includes可能会接受一个number,我想,这意味着这里有一个类型错误吗?
任何见解,为什么这不工作,以及如何使它的工作是赞赏!

3zwtqj6y

3zwtqj6y1#

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转换成一个并集(为了把keyof T转换成Exclude),这是用K[number]完成的,我想这是不言自明的,这和T[keyof T]创建T的值的并集是一样的。
Playground

dwbf0jvd

dwbf0jvd2#

最简单的办法:

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 _
}
h5qlskok

h5qlskok3#

上面Nurbol给出的公认答案可能是类型化程度更高的版本,但下面是我在utils-min中所做的事情。
它使用了内置的类型脚本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;
}
kwvwclae

kwvwclae4#

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];
    };
}
unguejic

unguejic5#

不确定我是否得到了一个点,但我遇到了类似的问题,我想确保我没有错字时,省略属性,所以我来到这样的解决方案:

export interface Person {
  id: string;
  firstName: string;
  lastName: string;
  password: string;
}

type LimitedDTO<K extends keyof Person> = Omit<Person, K>;

export type PersonDTO = LimitedDTO<"password" | "lastName">;

tsc不允许您忽略Person接口上不存在的属性

dfuffjeb

dfuffjeb6#

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

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;
// }
ipakzgxi

ipakzgxi7#

不幸的是,不可能摆脱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

3htmauhk

3htmauhk8#

使用数组reduce方法省略该道具。

const omitProps = <T extends object, K extends keyof T>(
  data: T,
  props: Array<K>
): Omit<T, K> => {
  if (!data || !Array.isArray(props) || !props.length) {
    return data;
  }
  return props.reduce((acc, prop) => {
    const { [prop as keyof object]: prop1, ...rest } = acc;
    return rest;
  }, data);
};

相关问题