TypeScript - 添加动态命名的 属性 到 return 类型

TypeScript - Add dynamically named property to return type

假设我有一个函数接受一个对象、一个键和一个值,然后 returns 一个新对象扩展原始对象添加键和值。

function addKeyValue(obj: Object, key: string, value: any) {
  return {
    ...obj,
    [key]: value
  }
}

问题: 我怎样才能输入这个函数,这样如果我这样调用它:

const user = addKeyValue({ name: 'Joe' }, 'age', 30);

编译器知道{user}的类型是{ name: string, age: number }

注意: 我知道我可以在没有函数的情况下做到这一点,但是这个问题的目标是用一个函数来完成。 (此示例基于一个更复杂的问题,为简洁起见进行了简化)。

这是我的想法,但它不起作用。 :(

function addKeyValue<TInput>(
    obj: TInput,
    key: string,
    value: any): TInput & { [key]: typeof value } {

    return {
        ...obj,
        [key]: value
    }
}

从调用者的角度来看,您可能希望签名类似于以下之一:

declare function addKeyValue<T extends {}, K extends keyof any, V>(obj: T, key: K, value: V): 
  T & Record<K, V>;
const user = addKeyValue({ name: 'Joe' }, 'age', 30); //  { name: string; } & Record<"age", number>
user.age // number
user.name // string

这是 TypeScript 2.8 之前的版本,其中交集是表示您正在做的事情的最佳方式。您可能对类型 {name: string} & Record<"age", number>} 不满意,但它的行为就像您想要的那样。

您也可以使用 conditional types:

declare function addKeyValue2<T extends {}, K extends keyof any, V>(obj: T, key: K, value: V):
  { [P in keyof (T & Record<K, any>)]: P extends K ? V : P extends keyof T ? T[P] : never };
const user2 = addKeyValue2({ name: 'Joe' }, 'age', 30); //  { age: number; name: string; } 
user2.age // number
user2.name // string

这具有 return 类型的优点,看起来像您期望的那样,但缺点是复杂且可能脆弱(可能并不总是像联合等那样表现)。

这个版本的另一个可能的优点是,如果keyobj的键重叠,属性类型将被覆盖:

const other2 = addKeyValue2({ name: 'Joe', age: 30 }, 'name', false); 
//  { name: boolean; age: number } 

而不是相交,这可能不是你想要的:

const whoops = addKeyValue({ name: 'Joe', age: 30 }, 'name', false).name; // never
// boolean & string ==> never

None 使得实现类型检查正确。为此,您应该使用任何 type assertions or overloads 可以完成的工作:

  return {
    ...(obj as any), // simplest way to silence the compiler
    [key]: value
  }

希望对您有所帮助。祝你好运!