2

for example, a function to map object keys to another ones.

function mapKeys<O extends Record<string, unknown>, K extends keyof O, KM extends Record<K, string>>(obj: O, keyMap: KM) {
  const r: Record<KM[keyof KM], O[K]> = {}
  for (const k of Object.keys(obj)) r[keyMap[k]] = obj[k]
  return r
}

the function will meet the error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<KM[keyof KM], any>'. No index signature with a parameter of type 'string' was found on type 'Record<KM[keyof KM], any>'.(7053)

As I know, it's because the property value type of keyMap is string instead of string literal.

const x = mapKeys({a: 1}, {a: 'b'} as const) 
/* 
'as const' instructs the compiler it's a literal, 
so here the `x.b` will correctly inferred to be `string`.
*/

But how can I make the compiler infer the object property values type to be the string literal instead of string when declare types? (when the source code is Javascript, it's not possible to use as const assertion on the value)

P.S. the keyMap is not sure. (So enum or union is not work.)

SOLVED:

To make the compiler infer the mapped keys in the output object, the key is to narrow keyMap with Partial<>:

function mapKeys<O extends Record<string, unknown>, 
K extends keyof O, 
KM extends Partial/* narrow keyMap */<Record<K, string>>>(obj: O, keyMap: KM) {
  const r: Record<KM[keyof KM], O[K]> = {}
  for (const k of Object.keys(obj)) r[keyMap[k]] = obj[k]
  return r
}

// here `x.b` will be inferred (as `a`'s type) without using `as const`
const x = mapKeys({a: 1}, {a: 'b'}) 
  • What do you mean by *"...(when the source code is Javascript, it's not possible to use as const assertion on the value)"*? I mean, that's true, but there's also no need to do that (because there's no static typing), so...? – T.J. Crowder Aug 02 '21 at 10:27
  • @T.J.Crowder, in other words, I want to achieve the same effect as `as const` in typescript with jsdoc or typing files. – user16574526 Aug 02 '21 at 10:32
  • it's useful for the intelliSense. – user16574526 Aug 02 '21 at 10:33
  • I believe you are asking about function argument inference. See my article https://catchts.com/infer-arguments . Let me know if it helps – captain-yossarian from Ukraine Aug 02 '21 at 13:24
  • To help with getting answers quickly, can you be more specific about the error's position: where exactly in the code is the error being raised? This can help others quickly identify the issue. In this case for example, by straight inspection, there are a few fixes that may easily apply but without taking the time to try your code in an env, it's hard to say which. Knowing where the error occurred exactly would help here. – otto-null Aug 02 '21 at 13:58

1 Answers1

2

Please let me know if this is what you are looking for:


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

/**
 * Get union of all values ov the object
 */
type Values<T> = T[keyof T]

/**
 * Iterate through the Dictionary - in our case KeyMap
 *  
 */
type Rename<Obj, Dictionary> =
  /**
   * Check if Dictionary is a Map structure
   */
  Dictionary extends Record<string, string>
  /**
   * Get all created key/values pair and merge them
   * Hence, we only using newly created key/values pairs.
   * Obj is not returned from this util type
   */
  ? UnionToIntersection<Values<{
    [Prop in keyof Dictionary]: Prop extends keyof Obj
    /**
     * Create new key/value pair
     */
    ? Record<Dictionary[Prop], Obj[Prop]>
    : never
  }>>
  : never

{
  // { customAge: 42; }
  type Test = Rename<{ age: 42 }, { age: 'customAge' }>

    // unknown - I don't know how you want to handle it
  type Test2 = Rename<{ age: 42 }, { notExists: 'customAge' }>

      // never - because second argument is not a dictionary
  type Test3 = Rename<{ age: 42 }, 42>
}

function mapKeys<
  /**
   * Apply constraint for all keys of Object
   */
  ObjKeys extends PropertyKey,
  /**
   * Apply constraint for all values of Object
   */
  ObjValues extends PropertyKey,
  /**
   * Infer Object with appropriate Keys and Values
   */
  Obj extends Record<ObjKeys, ObjValues>,
  /**
   * Apply constraint for Key of KeyMap
   */
  Keys extends keyof Obj,
  /**
   * Aplly constraint for KeyMap keys
   */
  NewKeys extends PropertyKey,
  /**
   * Infer KeyMap, Keys should extend Object keys
   */
  KeyMap extends Record<Keys, NewKeys>
>(obj: Obj, keyMap: KeyMap): Rename<Obj, KeyMap>
function mapKeys<
  ObjKeys extends PropertyKey,
  ObjValues extends PropertyKey,
  Obj extends Record<ObjKeys, ObjValues>,
  NewKeys extends PropertyKey,
  KeyMap extends Record<keyof Obj, NewKeys>,
  >(obj: Obj, keyMap: KeyMap) {
  return (Object.keys(obj) as Array<keyof Obj>)
    .reduce((acc, elem) => ({
      ...acc,
      [keyMap[elem]]: obj[elem]
    }), {} as Record<PropertyKey, Values<Obj>>)
}


const result = mapKeys({ age: 1 }, { "age": "newAge" })
result.newAge // 1

Playground If yes - I will provide explanation

Please keep in mind, r[keyMap[k]] mutations are not the best option if you want to make it type safe. TS does not track mutations. See my article