typescript 有没有更干净的方法来更新T**的**keyof值?

xqkwcwgp  于 2023-10-22  发布在  TypeScript
关注(0)|答案(2)|浏览(122)

我正在尝试将属性值从源S复制到目标T。在以编程方式检查字段是否存在后,我希望TS允许我访问它,但不会:

● Test suite failed to run

    src/util/Copy.ts:18:18 - error TS2536: Type 'keyof T' cannot be used to index type 'S'.

    18           return source[field] === target[field];
                        ~~~~~~~~~~~~~
    src/util/Copy.ts:33:11 - error TS2536: Type 'keyof S' cannot be used to index type 'T'.

    33           match[field] = source[field];
                 ~~~~~~~~~~~~

我可以让源从目标扩展到只接受两种类型的字段。但是我的实用方法甚至可以处理完全不同的类型。下面是失败的代码

export function copyFieldsWhenMatching<S extends object, T extends object>(
  sourceArray: S[],
  targetArray: T[],
  equalFields: (keyof T)[],
  fieldsToCopy: (keyof S)[],
  allowMultipleOccurrences: boolean
): T[] {
  const toReturn = [...targetArray];

  for (const source of sourceArray) {
    // Find matches based on equalFields
    const matches = toReturn.filter((target) => {
      return equalFields.every((field) => {
        // eslint-disable-next-line no-prototype-builtins
        if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) {
          return source[field] === target[field];
        }
        return true; // Property doesn't exist in either source or target; continue matching.
      });
    });

    if (!allowMultipleOccurrences && matches.length > 1) {
      throw new DuplicateMatchesError(
        "More than one match found for the source object.",
        matches
      );
    } else if (matches.length >= 1 || allowMultipleOccurrences) {
      // Copy fields from source to matches
      for (const match of matches) {
        for (const field of fieldsToCopy) {
          match[field] = source[field];
        }
      }
    }
  }

  return toReturn;
}

这就是我如何破解它通过(铸造目标源和反向只是为了通过)。这就引出了标题中的问题:
有没有更干净的方法来更新Tkeyof值?

import { DuplicateMatchesError } from "../errors";

export function copyFieldsWhenMatching<S extends object, T extends object>(
  sourceArray: S[],
  targetArray: T[],
  equalFields: (keyof T)[],
  fieldsToCopy: (keyof S)[],
  allowMultipleOccurrences: boolean,
): T[] {
  const toReturn = [...targetArray];

  for (const source of sourceArray) {
    // Find matches based on equalFields
    const matches = toReturn.filter((target) => {
      return equalFields.every((field) => {
        // eslint-disable-next-line no-prototype-builtins
        if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) {
          return (
            // dirty assertion to hack TS
            (source as unknown as T)[field] ===
            target[field]
          );
        }
        return true; // Property doesn't exist in either source or target; continue matching.
      });
    });

    if (!allowMultipleOccurrences && matches.length > 1) {
      throw new DuplicateMatchesError(
        "More than one match found for the source object.",
        matches,
      );
    } else if (matches.length >= 1 || allowMultipleOccurrences) {
      // Copy fields from source to matches
      for (const match of matches) {
        for (const field of fieldsToCopy) {
          // dirty assertion to hack TS
          (match as unknown as S)[field] = source[field];
        }
      }
    }
  }

  return toReturn;
}
a8jjtwal

a8jjtwal1#

我建议将所有数组强制转换为(S & T)[]

export function copyFieldsWhenMatching<S extends object, T extends object>(
  sourceArray: S[],
  targetArray: T[],
  equalFields: (keyof T)[],
  fieldsToCopy: (keyof S)[],
  allowMultipleOccurrences: boolean,
): T[] {
  const toReturn = [...targetArray] as (S & T)[];

  for (const source of (sourceArray as (S & T)[])) {
    // Find matches based on equalFields
    const matches = toReturn.filter((target) => {
      return equalFields.every((field) => {
        if (Object.hasOwn(source, field) && Object.hasOwn(target, field)) {
          return source[field] === target[field]
        }
        return true; // Property doesn't exist in either source or target; continue matching.
      });
    });

    if (!allowMultipleOccurrences && matches.length > 1) {
      throw new Error(
        "More than one match found for the source object.",
        { cause: matches },
      );
    }
    if (matches.length === 1 || matches.length > 1 && allowMultipleOccurrences) {
      // Copy fields from source to matches
      for (const match of matches) {
        for (const field of fieldsToCopy) {
          match[field] = source[field];
        }
      }
    }
  }

  return toReturn;
}
pbossiut

pbossiut2#

if (source.hasOwnProperty(field)) {
  return source[field] === target[field];
}

在示例中的这段代码中,您使用hasOwnProperty()来检查source是否具有属性。但是,hasOwnProperty()不会缩小其目标,因此TS不会缩小source以包含field指定的属性。
要解决这个问题,您可以创建一个 * 用户定义的类型保护 *,它调用hasOwnProperty()并缩小目标对象。
TypeScript在某些情况下缩小了变量的类型,例如使用typeof时。通过使用一个名为 type predicate 的返回类型,我们可以创建 functions 来缩小其输入的类型。这些被称为 * 用户定义的类型保护 *。
下面是hasOwnProperty()的例子:

function hasOwnProperty<O extends object, K extends PropertyKey>(
  obj: O,
  key: K,
): obj is O & Record<K, unknown> {
  return Object.prototype.hasOwnProperty.call(obj, key)
}

你可以这样使用它:

if (hasOwnProperty(source, field)) {
  return source[field] === target[field]
}

注意:这是“意味着”与字符串键一起使用。由于field的类型为keyof T,因此它实际上将source类型为S & Record<keyof T, unknown>(即具有T的所有属性)。但是,由于只是使用相同的变量(field)来索引source,所以没有问题。但要小心,如果你做得更多。
这是你的代码:TypeScript Playground。
有关类型保护的更多信息,请参阅TS文档和优秀的TypeScript Deep Dive by basarat

相关问题