Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

How is TS able to resolve infinitely recursive type correctly?

Recently I wrote a type that replaces all booleans and numbers in an object with strings.

type AllStrings<T> = {
    [Key in keyof T]:
        T[Key] extends number
            ? string
            : T[Key] extends boolean
            ? string
            : AllStrings<T[Key]>;
}

What I do not understand, why is it working correctly without an edge case like T[Key] extends string ? string : AllStrings<T[Key]>.

If I use this type like so:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

interface Person { name: string }
type NewPerson = AllStrings<Person>

Why does not it go into an infinite loop? Why TS understands that name in NewPerson is a string? Is this behavior documented somewhere? Is it intended or will be possible that it breaks with another version?

>Solution :

Why does it work without an edge case (filter) ?

When mapping over Person, the AllStrings type will also go over name: string and determine that it is neither a number nor a boolean. It will then call itself with AllStrings<string>. You might assume that execution will continue here since the string type also has lots of properties to map over. But in fact, mapping over primitive types will just evaluate to the primitve itself. You can read about that in the (lesser known) TypeScript-FAQ.

From the FAQ:

Mapped types declared as { [ K in keyof T ]: U } where T is a type parameter are known as homomorphic mapped types, which means that the mapped type is a structure preserving function of T. When type parameter T is instantiated with a primitive type the mapped type evaluates to the same primitive.

So AllStrings<string> just evaluates to string which ends the recursion.

Will be possible that it breaks with another version?

This will likely not break in another version. But be aware that your type breaks in other scenarios. Imagine you are passing a Person with a Date property.

interface Person { name: Date }

type NewPerson = AllStrings<Person>

Date is not a primitve so the mapped type will map over the Date and will replace all the number and boolean properties. TypeScript does not do us a favor in properly displaying the type here. But you can check what happens when you pass a Date to AllStrings.

type NewPerson2 = AllStrings<Date>
// type NewPerson2 = {
//     toString: AllStrings<() => string>;
//     toDateString: AllStrings<() => string>;
//     toTimeString: AllStrings<() => string>;
//     toLocaleString: AllStrings<{
//         (): string;
//         (locales?: string | ... 1 more ... | undefined, options?: Intl.DateTimeFormatOptions | undefined): string;
//     }>;
//     ... 40 more ...;
//     [Symbol.toPrimitive]: AllStrings<...>;
// }

Mapping recursively through a Date and changing its types is probably not what your type was intended to do.


Playground

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading