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
