Robert Koritnik
Robert Koritnik

Reputation: 105081

Typescript generic "cast" function

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?

Edit

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>);
}

Here's some code to play with

Upvotes: 0

Views: 317

Answers (1)

Robert Koritnik
Robert Koritnik

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

Related Questions