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

man*_*roe 1 generics typescript

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

这是一个TypeScript游乐场链接,具有相同的演练,但使查看编译器错误更容易。

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

type TTrueOrFalse = 'true' | 'false'
const toBool = (tf: TTrueOrFalse): boolean => tf === 'true'
Run Code Online (Sandbox Code Playgroud)

我们有一些要处理的字段。有些是数字,有些是我们用表示的类似复选框的值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
}
Run Code Online (Sandbox Code Playgroud)

此功能可以一次处理一个表单字段。我们必须将转换TTrueOrFalseboolean实现此功能。

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

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

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)
    )
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,尽管内部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
Run Code Online (Sandbox Code Playgroud)

通过这一切,我来承担问题的一部分是我的东西传递到handleSubmitfieldName仍然可能是联盟类型'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)
Run Code Online (Sandbox Code Playgroud)

理想情况下,我可以handleSubmit动态调用的唯一方法是在类型的对象上进行映射,TFormDatahandleSubmit使用编译器已知为正确类型的每个键/值对进行调用。

我真正想要定义的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
Run Code Online (Sandbox Code Playgroud)

有没有一种方法可以handleSubmit在函数内部和外部实现类型安全性而无需强制转换?

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

const handleSubmitForWholeForm = (
  formField: keyof TFormData,
  form: TFormData
) => {
  if (formField === 'yesOrNoQuestion') {
    submitFormField(formField, toBool(form[formField]))
  }
}
Run Code Online (Sandbox Code Playgroud)

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

jca*_*alz 5

TypeScript 尚不知道如何通过控制流分析来缩小类型参数 。这意味着,如果您使handleSubmit()函数在字段名称类型中通用,则N检查的值本身fieldName不会缩小N,因此TFormData[N]也不会缩小的类型。

一种可行的处理方式是使函数具体而不是通用。但是,如何使fieldNamevalue参数保持关联?我们可以使用rest参数元组。具体来说,如果我们AllParams定义如下类型:

type AllParams = { [N in keyof TFormData]: [N, TFormData[N]] }[keyof TFormData]
// type AllParams = ["yesOrNoQuestion", TTrueOrFalse] | ["freeformQuestion", number]
Run Code Online (Sandbox Code Playgroud)

然后我们可以做handleSubmit类似的签名(...nv: AllParams) => voidAllParams是和的所有可接受对的fieldName并集value(并且上面的定义应该以更长的形式缩放)。

这是handleSubmit()实现:

const handleSubmit = (...nv: AllParams) => {
  if (nv[0] === "yesOrNoQuestion") {
    submitFormField(nv[0], toBool(nv[1]));
  } else {
    submitFormField(nv[0], nv[1]);
  }
}
Run Code Online (Sandbox Code Playgroud)

您无法将其nv分解为单独的fieldNamevalue变量,否则它们之间的相关性将会丢失。相反,您必须使用nv[0]nv[1]依靠控制流分析来nv基于测试缩小范围nv[0],如上所示。

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

handleSubmit('yesOrNoQuestion', 'false') // okay
handleSubmit('yesOrNoQuestion', 'wont work') // error
handleSubmit('freeformQuestion', 3); // okay
handleSubmit(Math.random() < 0.5 ? 'yesOrNoQuestion' : 'freeformQuestion', 1); // error
Run Code Online (Sandbox Code Playgroud)

就是说,我通常处理传递给函数的相关类型的方法是使用一些明智的类型断言,就像您发现自己在原始handleSubmit()实现中那样。如果您希望拥有非休息参数功能签名的便利,则可以toBool(value as any as TTrueOrFlase)继续使用并继续。


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

链接到代码