Pesquisa de site

Em Typescript, existe uma maneira de declarar um tipo literal além de usar a asserção `as const` para um valor específico?


por exemplo, uma função para mapear chaves de objeto para outras.

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
}

a função encontrará o erro: Element implicitamente tem um tipo 'any' porque a expressão do tipo 'string' não pode ser usada para indexar o tipo 'Record<KM[keyof KM], any>'. Nenhuma assinatura de índice com um parâmetro do tipo 'string' foi encontrada no tipo 'Record<KM[keyof KM], any>'.(7053)

Até onde eu sei, é porque o tipo de valor da propriedade de keyMap é string em vez de literal de string.

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`.
*/

Mas como posso fazer o compilador inferir o tipo dos valores das propriedades do objeto como literal de string em vez de string ao declarar tipos? (quando o código-fonte é Javascript, não é possível usar a asserção as const no valor)

P.S. o keyMap não está certo. (Então enum ou union não funcionam.)

RESOLVIDO :

Para fazer o compilador inferir as chaves mapeadas no objeto de saída, a chave é restringir keyMap com 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'}) 

Resposta #1

Por favor, me avise se isso é o que você está procurando:

// 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 Se sim - eu fornecerei explicação

Por favor, tenha em mente que mutações em r[keyMap[k]] não são a melhor opção se você quer torná-lo seguro para tipos. O TS não rastreia mutações. Veja meu artigo