Typescript: change type of object properties and nested object properties to one type
Asked Answered
S

4

5

Having this example:

interface Event {
  title:string;
  description:string;
  fromDate: Date;
  toDate: Date;
  location: {
    name: string;
    lat: number;
    long: number;
  }
}

Using a type something like PropertiesToString<Event> I expect to return this type:

{
  title:string;
  description:string;
  fromDate: string;
  toDate: string;
  location: {
    name: string;
    lat: string;
    long: string;
  }
}

The question is how do I create the PropertiesToString<T> type?

I've managed to create something that works but not for nested object. If i have an nested object instead of modifing the object properties to string, it sets the object to string.

This is my version which doesn't work for nested objects, because instead of changing the type of location properties to string , it changes the type for location itself to string:

export type RequestBody<T> = {
  [P in keyof T]: string;
};
Subpoena answered 22/3, 2021 at 17:39 Comment(0)
C
6

type ToString converts a given type to string. PropertiesToString iterate over each key of the type passed and change its type to string using ToString. You can add other special cases that you want to handle in ToString using the type ternary operator.

interface Event1 {
    title: string;
    description: string;
    fromDate: Date;
    toDate: Date;
    location: {
        name: string;
        lat: number;
        long: number;
  }
}

type ToString<T> = T extends Date
    ? string
    : T extends object
    ? PropertiesToString<T>
    : string

type PropertiesToString<T> = {
    [K in keyof T]: ToString<T[K]>
}

type Generated = PropertiesToString<Event1>

type X = PropertiesToString<Event1['location']>

const x: Generated = {
    title: 'lorem',
    description: 'lorem',
    fromDate: 'lorem',
    toDate: 'lorem',
    location: {
        name: 'lorem',
        lat: 'lorem',
        long: 'lorem',
    }
}

Playground

Cream answered 22/3, 2021 at 17:57 Comment(4)
Manually checking for each type seems pretty heavy handed. You need a bunch more cases to make it work (boolean? date? etc)Vinculum
@Vinculum Hm, that can be a problem. And your answer cleverly handles that. Anyway, my answer still can be helpful and seems more "modular". One can wish to change any type to string.Cream
@Tim, I have updated the answer. It need not check every type manually anymore. If it is failing somewhere please let me know, otherwise please undo the downvote :)Cream
Hi, you saved my day! Howerver, I think that the type X = PropertiesToString<Event1['location']>part is a bit confusing since it doesn't have nothing to do with the restCoachman
V
2

You are actually really close. However, there will be a few edge cases you will likely have to handle. In JS lots of things are objects, and you likely don't want them all to simply turn into strings. So you will probably have to enhance this with some more logic. But at it's simplest

type RecursiveObject<T> = T extends Date ? never : T extends object ? T : never; 
export type StringValues<TModel> = {
    [Key in keyof TModel]: TModel[Key] extends RecursiveObject<TModel[Key]> ? StringValues<TModel[Key]> : string;
};

Add in any special cases (Array? Other wrapper types?) that you need to handle in your code. Eventually we will get "not" handling in types, and this will be much simpler.

Vinculum answered 22/3, 2021 at 17:57 Comment(1)
Thank you a lot! I like this version more because its more simple and cleaner. :)Subpoena
M
0

Another approach can be:

// Do not control Date type here
type RecursiveObject<T> = T extends Date? never : T extends object ? T : never;
// Change properties (nested, or not) OT (Old Type) to NT (New Type)
type ToNewType<T, NT> = { [K in keyof T]: NT };
type NestedTypeChange<T, OT, NT> = {
    [k in keyof T]: T[k] extends OT ? NT : T[k] extends RecursiveObject<T[k]> ? ToNewType<NestedTypeChange<T[k], OT, NT>, NT> : NT
}
Mccready answered 19/2, 2022 at 3:27 Comment(1)
I believe there is mistake, the NestedTypeChange always changes all types to the new one (NT). Change NT to T[k] at the end of the line too keep the old types where there is no match.Declared
K
0

Instead of having PropertiesToString<Event>, you can simply write a function like this:

const eventToString = (e: Event) => ({
  ...e,
  formDate: e.fromDate.toString(),
  toDate: e.toDate.toString(),
  location: {
    ...e.location,
    lat: String(e.location.lat),
    long: String(e.location.long)
  }
})

type EventStringed = ReturnType<typeof eventToString>

It has some advantages:

  • It is more simple than engaging advanced TS features.
  • You have JS function to convert events.
  • You have more access to change object properties types other than simply string.
Komatik answered 21/11, 2023 at 6:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.