To tackle this issue, we can transform the C
class itself into a generic class with a type parameter T
that corresponds to the type of either obj1
or obj2
. By utilizing mapped types, we can effectively represent "same keys but different/related values". Here is an example:
type MappedValues<T> = { [K in keyof T]: Values<T[K]> }
class C<T> {
obj1: MappedValues<T>
obj2: T;
constructor(o: MappedValues<T>) {
this.obj1 = o;
this.obj2 = unwrapMap(o);
}
}
In the above code snippet, we declare that the type T
represents the type of obj2
, while obj1
has the type MappedValues<T>
, which is a mapped type where each property of obj1
is associated with a Values
-version.
Depending on how Values
and related types are implemented and declared, like so:
type Values<T> = { value: T };
function createValue<T>(value: T): Values<T> {
return { value };
}
function extractValue<T>(val: Values<T>): T {
return val.value;
}
function unwrapMap<T>(x: MappedValues<T>): T {
return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, extractValue(v)])) as any as T;
}
We can confirm the functionality by testing it out:
const c = new C({ key1: createValue(123), key2: createValue("hello"), key3: createValue(true) });
/* const c: C<{ key1: number; key2: string; key3: boolean;}> */
c.obj1.key1.value.toFixed(); // no errors
c.obj2.key2.toUpperCase(); // no errors
Playground link for more details