如何使该通用TypeScript函数按预期工作?

时间:2019-10-11 20:19:56

标签: typescript generics

我试图定义一个与TypeScript中的类型系统配合使用的函数,这样我就可以获取对象的键,并且如果该键的值需要修改(转换自定义string类型到我的示例中的boolean),我可以不进行类型转换。

Here's a TypeScript playground link具有相同的演练,但更易于查看编译器错误。

一些帮助程序类型可以开始我的示例:

type TTrueOrFalse = 'true' | 'false'
const toBool = (tf: TTrueOrFalse): boolean => tf === 'true'

我们要处理一些字段。有些是数字,有些是我们用TTrueOrFalse表示的类似复选框的值。

type TFormData = {
  yesOrNoQuestion: TTrueOrFalse
  freeformQuestion: number
}

// same keys as TFormData, but convert TTrueOrFalse to a bool instead, e.g. for a JSON API
type TSubmitFormToApi = {
  yesOrNoQuestion: boolean
  freeformQuestion: number
}

此功能一次可处理一个表单字段。我们必须为此功能将TTrueOrFalse转换为boolean

const submitFormField = <FieldName extends keyof TFormData>(
    fieldName: FieldName,
    value: TSubmitFormToApi[FieldName]
) => { /* some code here */}

这是问题所在。通过首先将TTrueOrFalse的值调整为booleans,此函数应采用一个表单字段及其值并将其发送给API。

const handleSubmit = <
    FieldName extends keyof TFormData
  >(
    fieldName: FieldName,
    value: TFormData[FieldName]
) => {
  // I want to convert `TTrueOrFalse` to a `bool` for my API, so I check if we are dealing with that field or not.
  // seems like this check should convince the compiler that the generic type `FieldName` is now `'yesOrNoQuestion'` and
  // that `value` must be `TFormData['yesOrNoQuestion']`, which is `TTrueOrFalse`.
  if (fieldName === 'yesOrNoQuestion') {

    // `value` should be interpreted as type `TTrueOrFalse` since we've confirmed `fieldName === 'yesOrNoQuestion'`, but it isn't
    submitFormField(
      fieldName,
      toBool(value) // type error
    )

    // Looks like the compiler doesn't believe `FieldName` has been narrowed down to `'yesOrNoQuestion'`
    // since even this cast doesn't work:
    submitFormField(
      fieldName,
      toBool(value as TTrueOrFalse) // type error
    )

    // so I'm forced to do this, which "works":
    submitFormField(
      fieldName as 'yesOrNoQuestion',
      toBool(value as TTrueOrFalse)
    )
  }

  // so I thought maybe I can use a manual type checking function, but it seems like
  // the fact that `FieldName` is a union of possible strings is somehow making what I want
  // to do here difficult?
  const isYesOrNo = (fn: FieldName): fn is 'yesOrNoQuestion' => fieldName === 'yesOrNoQuestion'

  // not referencing the generic type from the function, FieldName, works here though:
  const isYesOrNoV2 = (fn: Extract<keyof TFormData, string>): fn is 'yesOrNoQuestion' => fieldName === 'yesOrNoQuestion'

  // ok, so let's try again.
  if (isYesOrNoV2(fieldName)) {
    // seems like now the compiler believes FieldName is narrowed, but that doesn't narrow
    // the subsequent type I defined for `value`: `TFormData[FieldName]`
    submitFormField(
      fieldName,
      toBool(value) // type error
    )

    // At least this seems to work now, but it still sucks:
    submitFormField(
      fieldName,
      toBool(value as TTrueOrFalse)
    )
  }
}

请注意,尽管handleSubmit的内部存在我要执行的操作的问题,但编译器至少从调用角度理解了我想要执行的操作:

handleSubmit('yesOrNoQuestion', 'false')
handleSubmit('yesOrNoQuestion', 'true')
handleSubmit('yesOrNoQuestion', 'should error') // fails as expected

handleSubmit('freeformQuestion', 'not a number') // fails as expected
handleSubmit('freeformQuestion', 32) 

handleSubmit('errorQuestion', 'should error') // fails as expected
handleSubmit('errorQuestion', 12) // fails as expected

通过所有这些,我已经假设问题的一部分是我传递给handleSubmit的{​​{1}}的东西仍然可能是这样的联合类型fieldName

'yesOrNoQuestion' | 'freeformQuestion'

理想情况下,我可以动态调用// (simulate not knowing the fieldName at compile time) const unknownFieldName: Extract<keyof TFormData, string> = new Date().getDay() % 2 === 0 ? 'yesOrNoQuestion' : 'freeformQuestion' // now these compile, problematically, because the expected value is of type `'true' | 'false' | number` // but I don't want this to be possible. handleSubmit(unknownFieldName, 2) 的唯一方法是通过映射handleSubmit类型的对象并用每个已知的键/值对调用TFormData编译器提供的正确类型。

我真正想为handleSubmit定义的是一个函数,它正好使用handleSubmit的一个键和键对应的值类型的值。我不想为TFormData定义允许采用联合类型的内容,但我不知道是否可能吗?

我认为函数重载可能会有所帮助,尽管将其定义为较长的表单类型会很痛苦:

fieldName

是否有一种方法可以在函数内部和外部实现类型安全性而无需强制转换来定义function handleSubmitOverload(fieldName: 'yesOrNoQuestion', value: TTrueOrFalse): void function handleSubmitOverload(fieldName: 'freeformQuestion', value: number): void function handleSubmitOverload<FieldName extends keyof TFormData>(fieldName: FieldName, value: TFormData[FieldName]): void { if (fieldName === 'yesOrNoQuestion') { // This still doesn't work, same problem inside the overloaded function since the // concrete implementation's parameter types have to be the same as the non-overloaded try above submitFormField( fieldName, toBool(value) // type error ) } } // still works from the outside: handleSubmitOverload('yesOrNoQuestion', 'false') handleSubmitOverload('yesOrNoQuestion', 'wont work') // fails as expected // At least the overloaded version does handle this other problem with our first attempt, // since it no longer accepts the union of value types when the field name's type is not specific enough handleSubmitOverload(unknownFieldName, 'false') // compiles handleSubmitOverload(unknownFieldName, 42) // compiles

编辑:我认为值得一提的是,我知道这样的方法会起作用:

handleSubmit

但这不是我基于这个问题的真实代码的结构。

1 个答案:

答案 0 :(得分:5)

TypeScript doesn't yet know how to narrow type parameters via control flow analysis 。这意味着,如果使handleSubmit()函数在字段名称类型N中通用,则检查fieldName的值不会使N本身变窄,因此{{ 1}}也不会缩小。

一种可行的处理方式是使函数具体而不是通用。但是,如何使TFormData[N]fieldName参数保持关联?我们可以使用rest parameter tuples。具体来说,如果我们将类型value定义如下:

AllParams

然后我们可以使type AllParams = { [N in keyof TFormData]: [N, TFormData[N]] }[keyof TFormData] // type AllParams = ["yesOrNoQuestion", TTrueOrFalse] | ["freeformQuestion", number] 的签名类似handleSubmit(...nv: AllParams) => voidAllParamsfieldName的所有可接受对的并集(并且上面的定义应该以更长的形式缩放)。

这是value的实现:

handleSubmit()

您无法将const handleSubmit = (...nv: AllParams) => { if (nv[0] === "yesOrNoQuestion") { submitFormField(nv[0], toBool(nv[1])); } else { submitFormField(nv[0], nv[1]); } } 分解为单独的nvfieldName变量,否则它们之间的相关性将会丢失。相反,您必须使用valuenv[0]并依靠控制流分析来基于测试nv[1]来缩小nv,如上所示。

此函数应像重载的函数一样工作,因为它仅接受正确类型的参数对,而不接受字段名的并集:

nv[0]

话虽这么说,我通常处理明智的correlated types处理传递给函数的方法type assertions,就像您发现自己在原始handleSubmit('yesOrNoQuestion', 'false') // okay handleSubmit('yesOrNoQuestion', 'wont work') // error handleSubmit('freeformQuestion', 3); // okay handleSubmit(Math.random() < 0.5 ? 'yesOrNoQuestion' : 'freeformQuestion', 1); // error 实现中一样。如果您希望拥有非休息参数功能签名的便利,则可以使用handleSubmit()并继续。


好的,希望能有所帮助;祝你好运!

Link to code