You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何更优雅地动态更新keyof T类型的属性值?

TypeScript跨类型字段复制的类型检查问题解决办法

问题描述

尝试将源类型<S>的属性值复制到目标类型<T>的对象中,即便通过代码检查字段存在,TypeScript仍抛出类型索引错误:

● 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];
                 ~~~~~~~~~~~~

需求是让工具函数支持完全不同的类型,不想通过让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];

  for (const source of sourceArray) {
    const matches = toReturn.filter((target) => {
      return equalFields.every((field) => {
        if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) {
          return source[field] === target[field];
        }
        return true;
      });
    });

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

  return toReturn;
}

临时修复方案

目前可以通过类型断言((source as unknown as T)等)绕过检查,但这种方式不够优雅,临时修复代码如下:

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) {
    const matches = toReturn.filter((target) => {
      return equalFields.every((field) => {
        if (source.hasOwnProperty(field) && target.hasOwnProperty(field)) {
          return (
            (source as unknown as T)[field] ===
            target[field]
          );
        }
        return true;
      });
    });

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

  return toReturn;
}

更优雅的类型安全实现

方案1:细化泛型约束

通过扩展泛型参数,明确equalFields是S和T的共有键,fieldsToCopy是存在于S且值类型可赋值给T的键,让TypeScript提前识别字段兼容性:

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

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

  for (const source of sourceArray) {
    const matches = toReturn.filter((target) => {
      return equalFields.every((field) => source[field] === target[field]);
    });

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

  return toReturn;
}

方案2:使用类型守卫细化内部类型

如果需要保留原函数的参数灵活性,可在内部添加类型守卫,让TypeScript在运行时检查的同时识别字段的跨类型兼容性:

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

  // 类型守卫:判断字段是否同时属于两个对象
  function isSharedKey<K extends string | number | symbol>(
    key: K,
    obj1: object,
    obj2: object
  ): key is K & keyof typeof obj1 & keyof typeof obj2 {
    return obj1.hasOwnProperty(key) && obj2.hasOwnProperty(key);
  }

  for (const source of sourceArray) {
    const matches = toReturn.filter((target) => {
      return equalFields.every((field) => {
        return isSharedKey(field, source, target) 
          ? source[field] === target[field] 
          : true;
      });
    });

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

  return toReturn;
}

方案对比

  • 方案1通过泛型约束提前限制参数范围,类型检查在函数调用时就生效,更符合TypeScript的类型安全设计理念。
  • 方案2保留了原函数的参数灵活性,通过内部类型守卫实现类型细化,避免了粗暴的unknown断言。

内容的提问来源于stack exchange,提问作者Zied Hamdi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 14:20:55