Ответ на "почему ругается" такой - на вход может быть подан объект с полями более узкого, чем string типами, типа такого:
type MyEntity = {
name: string,
type: 'Type1' | 'Type2'
}
и на выходе type не будет соответствовать исходному типу, будет просто string. Так что по хорошему должно быть что-то типа
export function lowercasedObject<T extends Record<string, unknown>, R = {
[K in keyof T]: T[K] extends string ? string : T[K]
}> (obj: T): R {
const res = {} as R
for (let k of (Object.keys(obj) as Extract<keyof R, string>[])) {
res[k] = (typeof obj[k] === 'string' ? obj[k].toLowerCase() : obj[k]) as R[typeof k]
}
return res
}
но и это не совсем то, ибо в общем случае сломаются union более узкого типа и не строки, что тоже надо как-то обрабатывать
UPD: в общем, нужно определять наследование string в два этапа, см
песочницу
в итоге получается что-то типа такого в различных вариациях
type ReplaceStringParts<T> = T extends string ? string : T;
export function lowercasedObject<T extends Record<string, unknown>, R = {
[K in keyof T]: ReplaceStringParts<T[K]>
}> (obj: T): R {
const res = {} as R
for (let k of (Object.keys(obj) as Extract<keyof R, string>[])) {
res[k] = (typeof obj[k] === 'string' ? obj[k].toLowerCase() : obj[k]) as R[typeof k]
}
return res
}