TypeScript:类型安全地重新映射对象属性

3

我想做什么

我想要编写一个JavaScript函数,可以通过第二个参数重新映射第一个参数的属性名。

我使用这个重映射函数来创建查询字符串参数。例如,从 { param1: 1, param2: 2, param3: 3}?p1=1&p2=2&p3=3

/**
 * @example
 *
 * const original = { a: 1, b: 'WOW', c: new Date(2019, 1, 1, 0, 0, 0) };  
 * const mapping = { a: 'hello', b: 'world', c: '!!!' };
 * 
 * > remap(original, mapping);
 * { hello: 1, world: 'WOW', '!!!': new Date(2019, 1, 1, 0, 0, 0) }
 */
const remap = (original, mapping) => {
  const remapped = {};
  Object.keys(original).forEach(k => {
    remapped[mapping[k]] = original[k];
  });
  return remapped;
};

我的有缺陷的代码

我尝试过以下代码,但是它是有缺陷的。

export const remap = <
  T extends { [key: string]: any },
  U extends { [P in keyof T]: string }
>(original: T, mapping: U) => {
  const remapped: any = {};

  Object.keys(original).forEach(k => {
    remapped[mapping[k]] = original[k];
  });

  // Problems
  // 1. remapped is declared as any, and cast required.
  // 2. All values are declared ad any.
  return remapped as { [P in keyof U]: any };
};

const remapped = remap(
  { a: 1, b: 'text', c: new Date() },
  { a: 'Hello', b: 'World', c: '!!!' }
);

console.info(remapped);

1
这看起来像是一个 X/Y 问题。你想要用 remap 函数做什么? - Sefe
谢谢您的回复,Sefe。我添加了一个重新映射函数的目的。 - edhiwo
1个回答

6
你可以正确输入此内容,但需要一些条件类型魔法:
// Converts object to tuples of [prop name,prop type]
// So { a: 'Hello', b: 'World', c: '!!!' }
// will be  [a, 'Hello'] | [b, 'World'] | [c, '!!!']
type TuplesFromObject<T> = {
    [P in keyof T]: [P, T[P]]
}[keyof T];
// Gets all property  keys of a specified value type
// So GetKeyByValue<{ a: 'Hello', b: 'World', c: '!!!' }, 'Hello'> = 'a'
type GetKeyByValue<T, V> = TuplesFromObject<T> extends infer TT ?
    TT extends [infer P, V] ? P : never : never;


export const remap = <
    T extends { [key: string]: any },
    V extends string, // needed to force string literal types for mapping values
    U extends { [P in keyof T]: V }
>(original: T, mapping: U) => {
    const remapped: any = {};

    Object.keys(original).forEach(k => {
        remapped[mapping[k]] = original[k];
    });
    return remapped as {
        // Take all the values in the map, 
        // so given { a: 'Hello', b: 'World', c: '!!!' }  U[keyof U] will produce 'Hello' | 'World' | '!!!'
        [P in U[keyof U]]: T[GetKeyByValue<U, P>] // Get the original type of the key in T by using GetKeyByValue to get to the original key
    };
};

const remapped = remap(
    { a: 1, b: 'text', c: new Date() },
    { a: 'Hello', b: 'World', c: '!!!' }
);
// const remapped: {
//     Hello: number;
//     World: string;
//     "!!!": Date;
// }

@Sefe 我只是提供了枪,人们如何使用它是他们自己的事情 :P。你说得对,复杂的条件类型很难理解。最好让它们保持简单,或者至少让它们的行为易于理解。话虽如此,我倾向于让我的代码尽可能地类型安全,所以我可能会使用上面的类型化的 remap - Titian Cernicova-Dragomir
在这些情况下,我会尝试寻找更容易输入的替代方案。在 .d.ts 文件中,我会使用花哨的类型注释来注释那些只考虑了 JS 的第三方代码(在我看来这总是公平竞争的)。 - Sefe
@Sefe 为什么不呢,这很有趣 :) 字面类型是被推断出来的(在其他情况下),对于被约束为字面基本类型(如字符串或数字)的泛型类型参数。 - Titian Cernicova-Dragomir
@TitianCernicova-Dragomir,我知道你已经两年没有回复了,但是你的代码有一部分是有效的,而我不知道为什么。我已经查找过了,但只找到了不完整的答案或超出重点的答案。 type GetKeyByValue<T, V> = TuplesFromObject<T> extends infer TT ? TT extends [infer P, V] ? P : never : never; 具体部分是 infer 推断类型本身。 type a = 'abc' | 'def' infer TT ? TT : never;type a = 'abc' | 'def' 有什么不同?因为如果我省略那部分,整个功能就会崩溃。 - Gârleanu Alexandru-Ștefan
1
@GârleanuAlexandru-Ștefan 我使用这个技巧来启用分布式条件类型。Infer TT 是为了获取一个类型参数,然后我可以在其上进行分发(仅在裸类型参数上发生分发)。 - Titian Cernicova-Dragomir
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接