3

I am trying to create a generic interface for a function that accepts three argument. When filling in the first argument, the options of the second one should be in the scope of the first. The third argument should then be in scope of the second argument.

My current code looks like this:

type SettingActionCreator<T> = <
  L extends T,
  K extends keyof L,
  C extends keyof L[K]
>(
  type: K,
  settingKey: keyof L[K],
  settingValue: L[K][C]
)

export interface SomeSettings {
  [Type.TypeA]: {
    settingA: "value1" | "value2";
    settingB: "value3 | "value4";
  };
  [Type.TypeB]: {
    ...
  };
  [Type.TypeC]: {
    ...
  };
}

const setSomeSetting: SettingActionCreator<SomeSettings> = (
  type,
  settingKey,
  settingValue
) =>
  // do something

So when I want to call setSomeSetting with arguments I expect that my scope becomes smaller for each argument I pass into it.

If for example I fill in the first argument, than the SettingActionCreator makes sure that the second argument (settingKey) can only be settingA or settingB. So far everything works as expected.

setSomeSetting(Type.TypeA, ....) // <-- settingKey accepts "settingA" and "settingB"

But for the third argument I can't seem to solve it. When I set the first argument and the second argument I would expect that the third argument would only accept the union options within the chosen settingKey argument. But instead of that I get all options within Type.TypeA. So both the settingValues of settingA and settingB

With this code:

setSomeSetting(Type.TypeA, "settingA", "value3") // <-- settingValue should only accept "value1" or "value2"

I would expect an error like:

TS2345: Argument of type '"value3"' is not assignable to parameter of type `"value1" | "value2"

But instead I get no type errors because it accepts both the values of settingA and settingB.

Does someone have a suggestion for what I am doing wrong?

3
  • 2
    Can you create a typescript playground link with a working example of the problem? typescriptlang.org/play It's a lot easier to answer a question like this with a working demo. Commented Aug 27, 2019 at 21:08
  • I'm not sure that what you want is entirely possible. The reason it's accepting both values of settingA and settingB is because they are all the values that fit the type system you've described. There's no way to narrow it down any further than that because that would require runtime information that doesn't exist when typescript is parsing. I suggest you rework your design so the typing system is less nested, or maybe consider just throwing a runtime error for the final option Commented Aug 27, 2019 at 21:11
  • Idk, I suspect there's got to be a way to make this sort of thing work. If not with normal arguments, then it's got to work with a curried function, right? Commented Aug 27, 2019 at 23:29

1 Answer 1

3

You need to prove to TypeScript that the settingKey is related to the settingValue - the settingKey: keyof L[K] in your declaration is confusing it (I'm not sure why - it feels like a bug that type C extends keyof L[K] != keyof L[K], but it's definitely a limitation of the type inference code either way).

Change the type of that argument to settingKey: C and TypeScript will be able to understand the relationship between the three arguments:

type SettingActionCreator<T> = <
  L extends T,
  K extends keyof L,
  C extends keyof L[K]
>(
  type: K,
  settingKey: C, // <-- changed from keyof L[K]
  settingValue: L[K][C]
) => void;


declare const Type: {
  readonly TypeA: unique symbol,
  readonly TypeB: unique symbol,
  readonly TypeC: unique symbol
}

export interface SomeSettings {
  [Type.TypeA]: {
    settingA1: "valueA1" | "valueA2";
    settingA2: "valueA3" | "valueA4";
  },
  [Type.TypeB]: {
    settingB1: "valueB1" | "valueB2",
    settingB2: "valueB3" | "valueB4"
  },
  [Type.TypeC]: {
    //
  }
}

const setSomeSetting: SettingActionCreator<SomeSettings> = (
  type,
  settingKey,
  settingValue
) => {}

This results in the kind of error you are looking for:

setSomeSetting(Type.TypeB, "settingB1", "valueA3")
// Argument of type '"valueA3"' is not assignable to parameter of type '"valueB1" | "valueB2"'.
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you very much, this works as expected. I'm still confused how C is different from keyof L[K].

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.