条件类型不能像 return 类型那样正常工作

Condtional type is not working properly as return type

我正在尝试为我的桌面应用程序制作一个设置系统。具有包含多个设置的配置,例如 add_while_pausedmin_charsstartup_settings(这就是问题所在)

因此,我正在尝试使用 class 为 get_valueupdate_valuetransform_value 等设置创建多个功能...

我要存档以下结果:

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

但是,get_value 抛出错误:

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)

这意味着方法中的 returned 值与 return 类型中的条件类型不匹配,但这是怎么回事?我很确定条件是正确的,我测试过没有问题。

HasChild<"min_chars", true, false> // false
HasChild<"add_while_paused", true, false> // false
HasChild<"startup_settings", true, false> // true

这是我的可重现示例:

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 for object instead but I did it like this because object would match functions and arrays, not just "true objects"

好吧,这可能不是您需要的,但我找到了解决方案。

问题在于,虽然 HasChild 确定了 return 类型,但您最终也会得到类似的结果:

public get_value(): number {
  if (this.child_name) {
    return config[this.base_name][this.child_name]
  }
  return config[this.base_name]
}

Typescript 确定两个 return 语句具有不同的类型(例如,SystemSettings | number),因此您会收到编译器错误。

我认为解决这个问题的唯一方法是创建两个实现,一个用于基本键,另一个用于子键,但它们共享一个公共接口。

并不是说这会增加一些冗长,但它似乎是类型安全的(假设您像我一样注释新 BaseSetting/ChildSetting 的类型。同样,您可以将其推断为类型,或使用类型守卫。实际上,我认为您在传递这些设置包装器时可能会遇到问题,因为这样做会丢失特定类型。您可能会成为 getting/setting 来自具有任意值的 UI 的值类型(如字符串),因此您需要清理值,这意味着在设置实现可以 get/set 值之前添加一些东西来转换值。这似乎使包装器的需要无效 class groks 是这样打字的,除非你可以依赖 JS 的强制。

备注:

  1. 我简化了 base_namechild_name 道具
  2. 我添加了 set_value 来充实用例
  3. 它可以编译,但我不确定它是否能解决您更广泛的问题;-)
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 }
}

interface Setting<T> {
  get_value(): T
  set_value(value: T): void
}

class BaseSetting<BaseKey extends keyof Config> implements Setting<Config[BaseKey]> {

  public constructor(public readonly base_name: BaseKey) {  }

  public get_value(): Config[BaseKey] {
    return config[this.base_name]
  }

  set_value(value: Config[BaseKey]): void {
    config[this.base_name] = value
  }
}

class ChildSetting<BaseKey extends keyof Config, ChildKey extends ChildKeysOf<BaseKey>> implements Setting<Config[BaseKey][ChildKey]> {

  public constructor(
    public readonly base_name: BaseKey, 
    public readonly child_name: ChildKey
  ) {  }

  public get_value(): Config[BaseKey][ChildKey] {
    return config[this.base_name][this.child_name]
  }

  set_value(value: Config[BaseKey][ChildKey]): void {
    config[this.base_name][this.child_name] = value
  }
}

const delay: Setting<number> = new ChildSetting("startup_settings", "delay")
delay.get_value()
delay.set_value(200)
const enabled: Setting<boolean> = new ChildSetting("startup_settings", "enabled")
enabled.get_value()
enabled.set_value(true)
const min_chars: Setting<number> = new BaseSetting("min_chars")
min_chars.get_value()
min_chars.set_value(10)