Home > other >  Typescript change property types recursively
Typescript change property types recursively

Time:04-14

I have an object that I want to "overwrite" certain types for recursively. This is a contrived example, but in this case, I want to take any type that is a number and convert it to a string.


type NumbersStringified<O extends Object> = {
  [K in keyof O]: O[K] extends Object ? NumbersStringified<O[K]> : O[K] extends number ? string : O[K];
};

type Original = {
    data1: number,
    data2: boolean,
    data3: string,
    data4: {
        data5: number,
        data6: boolean,
        data7: string
    }
};

const converted: NumbersStringified<Original> = {
    data1: 'a',
    data2: false,
    data3: '',
    data4: {
        data5: 'a',
        data6: false,
        data7: ''
    }
};

TS Playground Link

In this example, I am expecting/wanting data1 and data5 to be strings but TS still thinks they should be a number. Does anyone know why the pattern of O[K] extends number ? string does not match data1 and data2?

Thanks for any help!

CodePudding user response:

You are using Object instead of object, so it's actually expecting the literal Object global. Changing it will make it work:

type NumbersStringified<O extends object> = {
  [K in keyof O]: O[K] extends object ? NumbersStringified<O[K]> : O[K] extends number ? string : O[K];
};

Playground

CodePudding user response:

The Object type does not mean what you think it means. It refers to any type that can be indexed into like an object. This includes auto-wrapped primitives. For example, string extends Object because, for example, "hello".toUpperCase() is allowed. Essentially only null and undefined do not extend Object. And that means your NumbersStringified type is pretty much a no-op; if O[K] is a number then O[K] extends Object and you never even reach the part where number is replaced with string.

If you want to refer to non-primitives, you should use the object type (note the difference in case).


That being said, you could write NumbersStringified without referring to Object or object at all, since mapped types will automatically behave like an identity function for primitives (e.g., string goes in, string comes out):

type NumbersStringified<T> = T extends number ? string : {
    [K in keyof T]: NumbersStringified<T[K]>
};

This is much simpler and gives you the behavior you want, at least for your example:

type N = NumbersStringified<Original>
/* type N = {
    data1: string;
    data2: boolean;
    data3: string;
    data4: {
        data5: string;
        data6: boolean;
        data7: string;
    };
} */

const converted: NumbersStringified<Original> = {
    data1: 'a',
    data2: false,
    data3: '',
    data4: {
        data5: 'a',
        data6: false,
        data7: ''
    }
}; // okay

Playground link to code

  • Related