I suppose the answer to this question as asked is that you can indeed write a recursive intersection in TypeScript like this since TypeScript 4.0:
type Foo = { a: string } & { [k: string]: Foo }; // works
declare const foo: Foo;
foo.a.toUpperCase();
foo.bar.a.toUpperCase();
Playground link to code
I am not going to go into detail here about why such a type is problematic, as OP already knows it and it is somewhat out of scope here; for those interested, you can read about in the Stack Overflow question
"How to define Typescript type as a dictionary of strings but with one numeric “id” property".
This should have worked since TypeScript 3.7 introduced support for more recursive type aliases, but there was a bug that prevented this case from working. It was reported in microsoft/TypeScript#38672 and fixed in microsoft/TypeScript#38673, which made it into TypeScript 4.0.
That means if you are getting errors for this code, you are using a version of TypeScript before 4.0, and might want to think about upgrading when you get an opportunity, as the language changes fairly rapidly.
If you must get such a thing working in an earlier version of TypeScript, you could do the following ugly thing:
type _Foo = { a: string, [k: string]: unknown };
type Foo = { [K in keyof _Foo]: string extends K ? Foo : _Foo[K] };
/* type Foo = {
[x: string]: Foo;
a: string;
} */
declare const foo: Foo;
foo.a.toUpperCase();
foo.bar.a.toUpperCase();
Playground link to code
You can trick the compiler into allowing an inconsistent index signature by mapping over a type with a consistent index signature and doing an inconsistent conditional type for the properties. So I take {a: string, [k: string]: unknown}
, a perfectly valid type, and map over it to produce Foo
, whose type looks is {[x: string]: Foo, a: string}
despite the fact that you couldn't write this type directly.
I don't really know if I'd recommend a solution that depends on tricking the compiler though, so you should still consider upgrading to TS4.0+ if possible, or maybe even abandoning this type altogether in favor of something TypeScript can handle.