TypeScript-添加动态命名的属性以返回类型

时间:2018-09-06 12:52:19

标签: typescript mapped-types

假设我有一个函数,该函数接受一个对象,一个键和一个值,然后返回一个新对象,该对象扩展了原始对象并添加了键和值。

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

1 个答案:

答案 0 :(得分:2)

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

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

这具有返回类型看起来像您期望的那样的优点,但是缺点是复杂且可能很脆弱(对于联合,其行为可能并不总是如预期的那样)。

此版本的另一个可能的优点是,如果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

这些都不能正确检查实现类型。为此,您应该使用type assertionsoverloads做的任何事情:

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

希望有帮助。祝你好运!