Home > database >  How to relate inner properties with first-level ones of an object type typed using an index signatur
How to relate inner properties with first-level ones of an object type typed using an index signatur

Time:12-29

Let's say we have the following object type:

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

type Obj = { 
    [K in "prop_a" | "prop_b" | "prop_c"]: {
        [P in `${K}${Digit}`]: unknown;
    }
}

Playground

By iterating on "prop_a" | "prop_b" | "prop_c" with K we can refer directly to each constituent of the union to impose the K-th prop as a prefix of each inner property of the corresponding sub-object.

The question is: is it possible to get a similar result when the properties of the first level are not known? If yes, how?

A trivial but incorrect attempt is the following:

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

type Obj = { 
    [K in string]: {
        [P in `${K}${Digit}`]: unknown;
    }
}

Playground

Because the prefix of an inner property is not constrained to the name of the corresponding key on the first level:

declare const obj: Obj;

// should error out because the prefix previous to the digit is not "whatever"
obj.whatever.it_should_error0;

// just the following should be accepted if the first level key is "whatever"
obj.whatever.whatever0;
obj.whatever.whatever1;
obj.whatever.whatever2;
obj.whatever.whatever3;
obj.whatever.whatever4;
obj.whatever.whatever5;
obj.whatever.whatever6;
obj.whatever.whatever7;
obj.whatever.whatever8;
obj.whatever.whatever9;

CodePudding user response:

No, there is no specific type in TypeScript that works this way. Mapping over string doesn't behave the way you want; it results in a string index signature that does not keep track of the particular key. Support for a type like you are describing was explicitly declined in microsoft/TypeScript#22509, so I don't think this will be possible in the foreseeable future.

You could possibly write Obj as a generic constraint:

type Obj<K extends string> = { [P in K]: { [Q in `${P}${Digit}`]: unknown } };
declare const obj: Obj<"whatever" | "otherThing">;
obj.whatever.whatever0 // okay
obj.whatever.it_should_error0; // error

You may not know the full key set at compile time, but if you know any keys you should be able to represent it this way (Obj<KnownKeys | UnknownKeys> can be safely widened/upcast to Obj<KnownKeys>, just like {foo: string, bar: number} can be safely widened to {foo: string}).

Otherwise the only approach I can think of which approximates what you're describing would be to refactor to use amethod instead of property access:

interface Obj {
    get<K extends string>(k: K): { [P in `${K}${Digit}`]: unknown };
}
declare const obj: Obj;
const objWhatever = obj.get("whatever");
objWhatever.whatever0; // okay
objWhatever.it_should_error0; // error

Playground link to code

  • Related