Home > Software design >  TypeScript return same nested object structure but with different types
TypeScript return same nested object structure but with different types

Time:09-13

I am working with this function which rebuilds the initial object, but casting every values to BigNumbers recursively.

export const castObjectValuesToBigNumber = function(initialObj: StringMap, nestedProp?: string) {

    let _castedObj: StringMap = {};

    for(const [property, value] of Object.entries(initialObj)) {

        if(typeof(value) === "object" && !(value instanceof BN)) {
            const newVal = castObjectValuesToBigNumber(value, property);
            _castedObj = { ..._castedObj, ...newVal };
        } else {
            if(nestedProp) {
                if(!_castedObj[nestedProp]) _castedObj[nestedProp] = {};
                _castedObj[nestedProp][property] = BigNumber.from(value.toString());
            } else {
                _castedObj[property] = BigNumber.from(value.toString());
            }
        }
    }

    return _castedObj;
}

I would like to make it so that the return type is same as the initialObj type but with end values as BigNumber type.

Example:

const myValue = {
   propertyA: new BN(105),
   propertyB: {
      propertyC: new BN(1),
      propertyD: new BN(2),
      propertyE: new BN(3)
   },
   propertyF: {
      propertyG: new BN(4)
   }
}

I would like that if I put this value as a parameter of my function, typescript automatically returns this type:

{
   propertyA: BigNumber,
   propertyB: {
      propertyC: BigNumber,
      propertyD: BigNumber,
      propertyE: BigNumber
   },
   propertyF: {
      propertyG: BigNumber
   }
}

I tried lots of things with generics types and nested types but I always have an error of some type.

Here is a Minimum Reproducible Example:

/////////////////////
/// DECLARATIONS

interface StringMap { [key: string]: any }

class BN {
    value;
    constructor(num: number | string) {
        this.value = num;
    }

    _isBN() {
        return true;
    }
}

class BigNumber {
    value;
    constructor(num: number | string) {
        this.value = num;
    }

    _isBigNumber() {
        return true;
    }
}

const myValue = {
   propertyA: new BN(105),
   propertyB: {
      propertyC: new BN(1),
      propertyD: new BN(2),
      propertyE: new BN(3)
   },
   propertyF: {
      propertyG: new BN(4)
   }
};

/////////////////////
/// FUNCTION
export const castObjectValuesToBigNumber = function(initialObj: StringMap, nestedProp?: string) {

    let _castedObj: StringMap = {};

    for(const [property, value] of Object.entries(initialObj)) {

        if(typeof(value) === "object" && !(value instanceof BN)) {
            const newVal = castObjectValuesToBigNumber(value, property);
            _castedObj = { ..._castedObj, ...newVal };
        } else {
            if(nestedProp) {
                if(!_castedObj[nestedProp]) _castedObj[nestedProp] = {};
                _castedObj[nestedProp][property] = new BigNumber(value.toString());
            } else {
                _castedObj[property] = new BigNumber(value.toString());
            }
        }
    }

    return _castedObj;
}

/////////////////////
/// TEST

/*
    Result is of type StringMap while I would like it to be :

    {
        propertyA: BigNumber,
        propertyB: {
            propertyC: BigNumber,
            propertyD: BigNumber,
            propertyE: BigNumber
        },
        propertyF: {
            propertyG: BigNumber
        }
    }
    
*/

const myCastedValue = castObjectValuesToBigNumber(myValue); 

CodePudding user response:

The type we'll use to recursively change each BN to BigNumber could look like this:

type CastToBigNumber<T> = T extends BN ? BigNumber : {
    [K in keyof T]: CastToBigNumber<T[K]>
};

If T is a BN, then we change it to BigNumber, otherwise it's an object and we change all its properties.

The function definition now looks like this, using a generic parameter:

export const castObjectValuesToBigNumber = function<Initial extends StringMap>(initialObj: Initial, nestedProp?: string): CastToBigNumber<Initial> {

Playground

  • Related