I'm trying to make a settings system for my desktop app. having a config that includes several settings like add_while_paused
, min_chars
, and startup_settings
(herein lies the problem)
So, using a class I'm trying to make several functionalities for settings like get_value
, update_value
, transform_value
, etc...
I want to archive the following results:
const a = new Setting("add_while_paused");
a.get_value(); // true
const b = new Setting("startup_settings"); // Error: Property 'child_name' is missing in type ...
b.get_value();
const c = new Setting("startup_settings", "delay")
c.get_value(); // 53
But, get_value
is throwing an error:
Type 'Config[BaseKey]' is not assignable to type 'HasChild<BaseKey, Config[BaseKey][ChildKey], Config[BaseKey]>'.
Type 'string | number | boolean | StartupSettings' is not assignable to type 'HasChild<BaseKey, Config[BaseKey][ChildKey], Config[BaseKey]>'.
Type 'string' is not assignable to type 'HasChild<BaseKey, Config[BaseKey][ChildKey], Config[BaseKey]>'.(2322)
This means that the returned value from the method is not matching the conditional type in the return type, but how is that? I'm pretty sure that the condition is right, I tested it and had no issues.
HasChild<"min_chars", true, false> // false
HasChild<"add_while_paused", true, false> // false
HasChild<"startup_settings", true, false> // true
Here's my reproducible example:
declare const $symbol: unique symbol;
interface NestedSetting {
[$symbol]?: never;
}
interface StartupSettings extends NestedSetting {
enabled: boolean;
delay: number;
}
interface Config {
add_while_paused: boolean;
min_chars: number;
note: string;
startup_settings: StartupSettings;
}
type BaseKeys = keyof Config;
type HasChild<BK extends BaseKeys, True, False> = Config[BK] extends NestedSetting
? True
: False;
type ChildKeysOf<BK extends BaseKeys> = HasChild<BK, keyof Config[BK], never>;
const config: Config = {
add_while_paused: true,
min_chars: 53,
note: "Hello World!",
startup_settings: { enabled: true, delay: 56 }
}
class Setting<BaseKey extends keyof Config, ChildKey extends ChildKeysOf<BaseKey> = never> {
public base_name: BaseKey;
public child_name: ChildKey;
public constructor(base_name: BaseKey, child_name: ChildKey) {
this.base_name = base_name;
this.child_name = child_name;
}
public get_value(): HasChild<BaseKey, Config[BaseKey][ChildKey], Config[BaseKey]> {
if (this.child_name) return config[this.base_name][this.child_name];
return config[this.base_name]
}
}
Also, please don't mind the
NestedSetting
type, I know I can just check forobject
instead but I did it like this becauseobject
would match functions and arrays, not just "true objects"
Well, it might not be what you need, but I found a solution.
The problem lies in the that whilst
HasChild
determines the return type, you end up with something akin too:Typescript determines that the two return statements have differing types (for example,
SystemSettings | number
), and hence you get the compiler error.The only way I can see to resolve this is to create two implementations, one for base keys, and another for child keys, but which share a common interface.
Not that this adds some verbosity, but it appears to be type safe (assuming you annotate the type of the new BaseSetting/ChildSetting as I have done. Equally, you could leave it inferred, or use a type guard. I actually think you might have problems passing these settings wrappers around because you'll lose the specific type in doing so. It's possible you are going to be getting/setting values from a UI that has an arbitrary type (like a string) and that you'll therefore need to sanitise the value, which means adding something to convert the value before the Settings implementation can get/set the value. This seems to invalidate the need for a wrapper class that groks type in this way unless you can rely on JS's coercion.
Notes:
base_name
andchild_name
propsset_value
to flesh out the use case