There is single interface IStorage where every key has two types:
interface IStorage {
key: boolean | CustomStore<boolean>,
key2: number | CustomStore<number>
}
type CustomStore<T> = {
value: T,
init: (value: T) => void;
set: (value: T) => void;
}
The class LiveStorage have defaultStorage (first type of IStorage) and storage (second):
class LiveStorage<D = { [key: string]: any }, S = { [key: string]: CustomStore<any> }> {
private defaultStorage: D
storage: S
constructor(defaultStorage: D) {
this.defaultStorage = defaultStorage as D
this.storage = {} as S
}
}
When create instance, key has both types:
let liveStorage = new LiveStorage<IStorage, IStorage>({})
liveStorage.storage.key // boolean | CustomStore<boolean>
Are there ways to key will have only CustomStore<boolean> by default? With single interface and without (liveStorage.storage.key as CustomStore<boolean>). I know that is only compile time, just want to get suggestion from IDE without (x as type) every time.
>Solution :
Essentially you want defaultStorage to be of a "base" key-value type, and then you want storage to be a transformed version of that type. For each property key K of the base type T, you want to take the property value type T[K] and map it to CustomStore<T[K]>. So we can define a mapped type that does this:
type CustomStorageFor<T> =
{ [K in keyof T]: CustomStore<T[K]> };
For your example then, you wouldn’t want to use your IStorage type directly, with unions of base and mapped properties. Instead, you would define IStorage to be the base type
interface IStorage {
key: boolean
key2: number
}
And then you get the CustomStorage version by mapping it:
type CustomStorageForIStorage = CustomStorageFor<IStorage>;
/* type CustomStorageForIStorage = {
key: CustomStore<boolean>;
key2: CustomStore<number>;
} */
Now your LiveStorage class can be generic in just the base type T, and storage will be given type CustomStorageFor<T>:
class LiveStorage<T extends object> {
private defaultStorage: T
storage: CustomStorageFor<T>;
constructor(defaultStorage: T) {
this.defaultStorage = defaultStorage
this.storage = {} as CustomStorageFor<T>; // you need to implement this
}
}
Now when you call the LiveStorage constructor, the compiler will infer the type argument T to be that of the constructor argument (and I’ve constrained T to the object type so that you’ll get an error if you try to pass in a string or something):
const defaultStorage: IStorage = { key: true, key2: 1 };
let liveStorage = new LiveStorage(defaultStorage);
// let liveStorage: LiveStorage<IStorage>
liveStorage.storage.key // CustomStore<boolean>
In fact you don’t even need the IStorage type if you don’t want to give it a name; the compiler will happily synthesize an equivalent object type from the constructor argument:
let liveStorage = new LiveStorage({ key: true, key2: 1 });
// let liveStorage: LiveStorage<{ key: boolean; key2: number; }>
liveStorage.storage.key // CustomStore<boolean>
Either way will work.