Reputation: 105081
I would like to write a function that allows me to cast all object properties (I only have plain objects) to specific type. For the time being I only want to cast to primitives boolean
, string
and number
. Something along these lines:
function castMembers<TObject extends object, TValue extends boolean | string | number>(instance: TObject, type: TValue): Record<keyof TObject, TValue> {
Object.keys(instance).reduce((result, k) => {
// ERROR
result[k] = new type(instance[k]).valueOf();
return result;
}, {});
}
In the future I may extend my casting to some whatever type as long as it satisfies my interface definition which would define that it must include valueOf
function. So...
I'm well aware that I can't really instantiate new objects based on generic types, as they're only for compile time. That's why I added the second parameter so I can provide constructor function that should be used for casting. But Im getting an error, or I invalidly wrote my TValue
generic param or the type of my second parameter.
How to write such a function?
I've tried introducing a constructor interface, which gets me very close but I still get an error:
interface IConstructor<TValue> {
new(value?: any): TValue;
}
function castMembers<TObject extends object, TValue extends number | string | boolean>(instance: TObject, toType: IConstructor<TValue>): Record<keyof TObject, TValue> | never {
return (Object.keys(instance) as (keyof TObject)[]).reduce((result, key) => {
result[key] = new toType(instance[key]).valueOf() as TValue;
return result;
}, {} as Record<keyof TObject, TValue>);
}
Upvotes: 0
Views: 317
Reputation: 105081
Actually I've solved it myself while playing around to make it work. And I finally did it.
This is the code.
interface IPrimitiveLike<T> {
valueOf(): T;
}
interface IConstructor<T> {
readonly prototype: IPrimitiveLike<T>;
new(value?: any): IPrimitiveLike<T>;
}
export default function castMembers<TObject extends object, TValue extends IPrimitiveLike<unknown>>(instance: TObject, toType: IConstructor<TValue>): Record<keyof TObject, TValue> | never {
return (Object.keys(instance) as (keyof TObject)[]).reduce((result, key) => {
result[key] = new toType(instance[key]).valueOf() as TValue;
return result;
}, {} as Record<keyof TObject, TValue>);
}
This code will correctly do:
let obj = {
text: 'test',
value: 123,
negative: 0,
bool: true
};
castMembers(obj, Boolean); // { text: true, value: true, negative: false, bool: true }
castMembers(obj, String); // { text: 'test', value: '123', negative: '0', bool: 'true' }
castMembers(obj, Custom); // also works if your custom class defines a valueOf() function
The only thing I'm not completely sure about is the use of unknown
type. It can of course be replaced by any
, but ideally it should be the primitive type related to the valueOf
return type.
Upvotes: 1