Issue with Abstract Concepts
My challenge involves relating two distinct groups of types to one another.
// Group A
interface Hello { ... }
interface Foo { ... }
// Group B
interface World { ... }
interface Bar { ... }
To tackle this problem, I am creating a third interface that serves as a mapping from one type to another:
interface LookupMap {
Hello: World;
Foo: Bar;
}
The goal is to navigate from one type to another using a single interface as an index for the LookupMap
(similar to how strings and numbers can be used for type lookups in indexed object literals):
type AltType<T> = LookupMap<T>;
const altHello: AltType<Hello> = ...; // intended to be of type World
const altFoo: AltType<Foo> = ...; // intended to be of type Bar
However, this approach does not work as types cannot be used as indexers in this manner.
Real-Life Scenario
My aim is to enhance typing in Immutable.js.
Beneath this point, there is some complicated code. If you already have a solution, you may skip it...
Immutable objects offer a variety of useful functions. For instance, let's focus on adding typing to Map.get
.
interface Immutalizer<MUTABLE_TYPE> extends Immutable.Map<keyof MUTABLE_TYPE, MUTABLE_TYPE[keyof MUTABLE_TYPE]> {
get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: MUTABLE_TYPE[PROP_NAME]): MUTABLE_TYPE[PROP_NAME];
}
interface MyInterface {
hello: boolean;
world: string;
foo: number;
bar: symbol;
}
interface MyImmutableInterface extends Immutalizer<MyInterface> {};
const myObject: MyImmutableInterface = Immutable.fromJS(...);
myObject.get("hello"); // boolean
myObject.get("world"); // string
myObject.get("foo"); // number
myObject.get("bar"); // symbol
Furthermore, if some properties are complex objects, an additional type must be provided to Immutalizer
for context:
interface Immutalizer<
MUTABLE_TYPE,
COMPLEX_OBJECT_KEYMAP extends { [PROP_NAME in keyof MUTABLE_TYPE]: any } = MUTABLE_TYPE
> extends Immutable.Map<
keyof MUTABLE_TYPE,
COMPLEX_OBJECT_KEYMAP[keyof MUTABLE_TYPE]
> {
get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: COMPLEX_OBJECT_KEYMAP[PROP_NAME]): COMPLEX_OBJECT_KEYMAP[PROP_NAME];
}
interface Hello {
foo: string;
bar: World;
}
interface World {
a: number;
b: symbol;
}
interface ImmutableHello extends Immutalizer<Hello, {
foo: string;
bar: ImmutableWorld;
}> {};
interface ImmutableWorld extends Immutalizer<World> {};
const myObject: ImmutableHello = Immutable.fromJS(...);
myObject.get("bar");
myObject.get("foo");
myObject.get("bar").get("a");
myObject.get("bar").get("b");
This process involves a lot of manual work, especially as the object tree deepens. I devised an alternative solution that is almost functional but not quite there yet:
type Primitive = string | number | boolean | symbol | String | Number | Boolean | Symbol;
type PrimitiveSwitch<PROP_TYPE, IMMUTALIZER_MAP extends ImmutalizerMap> =
PROP_TYPE extends Primitive ?
PROP_TYPE :
IMMUTALIZER_MAP[PROP_TYPE]; // TYPE ERROR: Type 'PROP_TYPE' cannot be used to index type 'IMMUTALIZER_MAP'.
interface ImmutalizerMap { [mutableType: string]: Immutalizer<any, this> }
interface Immutalizer<MUTABLE_TYPE, IMMUTALIZER_MAP extends ImmutalizerMap> {
get<PROP_NAME extends keyof MUTABLE_TYPE>(
prop: PROP_NAME,
notSetValue?: PrimitiveSwitch<MUTABLE_TYPE[PROP_NAME], IMMUTALIZER_MAP>
): PrimitiveSwitch<MUTABLE_TYPE[PROP_NAME], IMMUTALIZER_MAP>;
}
export interface Hello {
foo: string;
bar: World;
}
export interface World {
a: number;
b: symbol;
}
interface ImmutableHello extends Immutalizer<Hello, ImmutalizerMap> { }
interface ImmutableWorld extends Immutalizer<World, ImmutalizerMap> { }
interface MyImmutalizerMap {
Hello: ImmutableHello;
World: ImmutableWorld;
}
const hello: ImmutableHello = Immutable.fromJS(...);
hello.get("foo");
hello.get("bar");
The Immutalizer
section is somewhat complex, but using it is now theoretically straightforward:
- Maintain a mapping of all types and their associated immutable types.
- Create immutable types using
Immutalizer
by passing in the base type and the relevantImmutalizerMap
it belongs to. Immutalizer
handles the rest, utilizingPrimitiveSwitch
to determine if a type needs to be looked up in theImmutalizerMap
or not.
Despite these efforts, accessing IMMUTALIZER_MAP[PROP_TYPE]
in PrimitiveSwitch
triggers a type error:
Type 'PROP_TYPE' cannot be used to index type 'IMMUTALIZER_MAP'.
Inquiry
Is it possible to use an interface (name) as indexers in other interfaces? Are there alternative solutions for improving typing in Immutable objects?