2

I have the following code:

enum ParamOneType {
  Round,
  Square,
}

interface PossibleValues {
  [ParamOneType.Round]: 'a' | 'b';
  [ParamOneType.Square]: 'c' | 'd';
}

const indexRound = {
  a: 'whatever',
  b: 'whatever',
};

const doSomething = <T extends ParamOneType>(
  paramOne: T,
  paramTwo: PossibleValues[T],
): void => {
  switch (paramOne) {
    case ParamOneType.Round: {
      // Type 'PossibleValues[T]' cannot be used to index type '{ a: string; b: string; }'
      const b = indexRound[paramTwo];
    }
  }
};

Why do I get the error Type 'PossibleValues[T]' cannot be used to index type '{ a: string; b: string; }' here? Intellisense does seem to pick up the function signature correctly, for example my autocomplete in VSCode shows:

const doSomething: <ParamOneType.Round>(paramOne: ParamOneType.Round, paramTwo: "a" | "b") => void

when calling the function as doSomething(ParamOneType.Round, 'a')

3
  • You get this error because paramTwo cannot be used to index indexRound. There is no information about PossibleValues[T] being a keyof typeof indexRound. Commented Jul 27, 2022 at 14:41
  • If you tell typescript that it is one like this: const b = indexRound[paramTwo as keyof typeof indexRound]; It will work as expected. Commented Jul 27, 2022 at 14:41
  • Yeah, but that’s just casting it. My confusion stems from the fact that if paramOne is Round, paramTwo can only be ‘a’ or ‘b’ according to the function signature. Shouldn’t these be seen as valid indexes? If I change it to the string itself there is no error. Commented Jul 27, 2022 at 16:00

1 Answer 1

2

For this example, where you do a switch on paramOne, I'd stay away from generics, which cannot be narrowed by control flow analysis. It's always possible that T could be the full ParamOneType type, at which point someone could call doSomething() with parameters you don't expect, like doSomething( Math.random()<0.99 ? ParamOneType.Round : ParamOneType.Square, "c"), which has a 99% chance of being bad. This makes narrowing inside generic functions tricky. It's a known issue, and there are multiple feature requests for some way to improve it. See microsoft/TypeScript#27808 or microsoft/TypeScript#33014 for more information.

Instead, you can give doSomething a rest parameter whose type is a discriminated union of rest tuple types, and immediately destructure into the two variables paramOne and paramTwo.

type DoSomethingParams =
 [paramOne: ParamOneType.Round, paramTwo: "a" | "b"] | 
 [paramOne: ParamOneType.Square, paramTwo: "c" | "d"]

const doSomething = ([paramOne, paramTwo]: DoSomethingParams): void => { }

You can see that this behaves as desired from the caller's side:

doSomething(ParamOneType.Round, "a") // okay
doSomething(ParamOneType.Round, "c") // error
doSomething(ParamOneType.Square, "c") // okay
doSomething(ParamOneType.Square, "b") // error

And since TypeScript 4.6 and above supports control flow analysis for destructured discriminated unions, you can switch on paramOne in the implementation and the compiler will automatically narrow paramTwo accordingly:

const doSomething = (...[paramOne, paramTwo]: DoSomethingParams): void => {
  switch (paramOne) {
    case ParamOneType.Round: {
      const b = indexRound[paramTwo]; // okay
    }
  }
};

Also note that you can generate the above DoSomethingParams type programmatically from PossibleValues, as follows:

type DoSomethingParams = { [T in ParamOneType]:
  [paramOne: T, paramTwo: PossibleValues[T]]
}[ParamOneType]

This is called a distributive object type (as coined in ms/TS#47109) where you immediately index into a mapped type to get a union of [paramOne: T, paramTwo: PossibleValues[T]] for every T in PossibleValues.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

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.