简体   繁体   English

使用 typescript 强制参数组合

[英]Enforcing parameter combinations with typescript

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; }'为什么我会收到错误Type 'PossibleValues[T]' cannot be used to index type '{ a: string; b: string; }' Type 'PossibleValues[T]' cannot be used to index type '{ a: string; b: string; }' Type 'PossibleValues[T]' cannot be used to index type '{ a: string; b: string; }' here? Type 'PossibleValues[T]' cannot be used to index type '{ a: string; b: string; }'在这里? Intellisense does seem to pick up the function signature correctly, for example my autocomplete in VSCode shows: Intellisense 似乎确实正确地选择了 function 签名,例如我在 VSCode 中的自动完成显示:

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

when calling the function as doSomething(ParamOneType.Round, 'a')当调用 function 作为doSomething(ParamOneType.Round, 'a')

For this example, where you do a switch on paramOne , I'd stay away from generics , which cannot be narrowed by control flow analysis .对于这个例子,你在paramOne上做一个switch ,我会远离generics ,它不能被控制流分析缩小 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. T总是有可能是完整的ParamOneType类型,此时有人可以使用您不期望的参数调用doSomething() ,例如doSomething( Math.random()<0.99? ParamOneType.Round: ParamOneType.Square, "c") ,它有 99% 的可能性是坏的。 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.有关详细信息,请参阅microsoft/ TypeScript#27808 或microsoft/TypeScript#33014

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 .相反,您可以给doSomething一个rest 参数,其类型是rest 元组类型的可区分联合,并立即解构为两个变量paramOneparamTwo

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:并且由于 TypeScript 4.6 及更高版本支持对解构的可区分联合的控制流分析,您可以在实现中打开paramOne ,编译器会自动缩小paramTwo相应的范围:

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:另请注意,您可以从PossibleValues以编程方式生成上述DoSomethingParams类型,如下所示:

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 .这称为分布式 object 类型(如在ms/TS#47109中创造的),您可以立即索引到映射类型以获得 [ paramOne [paramOne: T, paramTwo: PossibleValues[T]]的联合,用于每个T中的PossibleValues

Playground link to code Playground 代码链接

You can just add a type definition to indexRound and your code should compile:您可以将类型定义添加到indexRound并且您的代码应该可以编译:

enum ParamOneType {
  Round,
  Square,
}

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

const indexRound: { [key: string]: string } = {
  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];
    }
  }
};

While you could use the as operator, this is usually considered bad practice.虽然您可以使用as运算符,但这通常被认为是不好的做法。 Also you should consider your code a little more since PossibleValues[T] can potentially be c or d which cannot be indexed on your indexRound object.此外,您应该多考虑一下您的代码,因为PossibleValues[T]可能是cd ,它们无法在您的indexRound object 上编入索引。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM