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

2024-02-17

我正在尝试定义一个与 TypeScript 中的类型系统配合良好的函数,这样我就可以获取对象的键,并且如果该键的值需要进行一些修改(转换自定义string键入一个boolean在我的示例中),我可以在不转换类型的情况下做到这一点。

它具有相同的演练,但更容易查看编译器错误。

一些帮助器类型可以让我的示例开始:

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 to a boolean对于这个功能。

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

问题就在这里。该函数应该采用一个表单字段及其值,然后通过首先调整将其发送到 APITTrueOrFalse价值观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)
    )
  }
}

请注意,虽然内部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 for 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') // error, no matching overload
handleSubmitOverload(unknownFieldName, 42) // error, no matching overload

有没有办法定义handleSubmit以一种无需强制转换即可在函数内部和外部实现类型安全的方式?

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

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

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


TypeScript 还不知道如何缩小范围类型参数通过控制流分析(参见微软/TypeScript#24085 https://github.com/microsoft/TypeScript/issues/24085) 。这意味着如果你让你的handleSubmit()字段名称类型中的函数通用N,检查值fieldName不会缩小N本身,因此类型TFormData[N]也不会被缩小。

一种可能的方法是使功能具体化而不是通用化。但我们怎样才能保持fieldNamevalue参数相关?我们可以用剩余参数元组 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#tuples-in-rest-parameters-and-spread-expressions。具体来说,如果我们创建一个类型AllParams定义如下:

type AllParams = { [N in keyof TFormData]: [N, TFormData[N]] }[keyof TFormData]
// type AllParams = ["yesOrNoQuestion", TTrueOrFalse] | ["freeformQuestion", number]

然后我们可以签名handleSubmit就像是(...nv: AllParams) => void. AllParams是所有可接受的对的并集fieldName and value(并且上面的定义应该随着更长的形式而扩展)。

这是handleSubmit()执行:

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

你无法解构nv分成单独的fieldName and value变量,否则它们之间的相关性将会丢失。相反,你必须使用nv[0] and 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

话虽这么说,我通常处理的方式相关类型 https://github.com/microsoft/TypeScript/issues/30581传递给函数是有一些明智的类型断言 https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions,正如您发现自己在原来的内部所做的那样handleSubmit()执行。如果您喜欢使用非休息参数函数签名的便利,您可以使用toBool(value as any as TTrueOrFlase)并继续前进。


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

链接到代码 http://www.typescriptlang.org/play/#code/C4TwDgpgBAKjBOBXCB5eAxAhgGwM7QF4oByYJCYqAHxIDMd9iAoAYwHsA7XYKYNgITZtsUIgApgtAFywEyNFjwQAlDIBGQ7BEwdRAPl61RBIqXLMmAektQA7tAAWmAG7RMUWhFseAlhGwAJrh20LY6PHxQYPBsLBC4uAB0UADKbAC2bvDQHIjpahDwuAA0ULgZWdAsDhAsANYaAB4AtNg+ddDOOMi4VjbATjz2UNnR8RAcQz4DUAAGcOQKDBCziUygkLDobPDpACKYwO5EAN5MUFAg8WgAcmwAij3APpwyC-IYy+ce2RC0O+lHvFnq8oLl8oUmABfJh9MqYTJQDogYKYYIwba7A5HUpqRA8dgcVzwHjvVCfJS8NhQdwaYRQHxcYDaAKlCCJADmyX+8BpUAAUikUDcoABBAAKAEl1uBoDAUog1Olppj0jA2KKwD5RFAzhcrrhbg8ni8OOpNNoON9aL8eYCTaDwQV4NDYdZeA4fMEvR7oLREBwWCDdANDnY2IhAlEYnEElBOH6Ab5-AEaTx3M9Mmt3cMnK4qVBCcSIjVZIsKfgC7TNB4dh6ff7A8G1oTuGVFcrgKr0H4o0QADwiiCNZkcIJIiAgNhGDEA7GYPRia29gI3BEQGQ3YrfLrYZBvBVKlUA9WanwAbRuAF0mMp9LqoJYAFRlCqFtgBRyFaBPmww2EbNAorYNg4qYPACLBKcUCXgyujItOWxzocmBXjIl6lLOWIoZeV5XlAULnghM6qvON7uoBYogWBEHpFBMEAEQGkaQLcKaDGYXI5KKPg+E0OeDE2hAfwAqxwYcWCeTOjerY8E4Y5aIenY6mIiRqUSMjAaB4GQXeBAGHqDJGGIRLngADPhJhEEx1zwHcYnsXehkXLgHbHrsPYpiZzjmVepR8IIwjeeeACMV7KMoADc3xQlA-iVs57ZHl2AKeYEwUWaUplhVFMWukw8kBIpbnAGIxDMXZxrAqaxClMQ9BKMQd7umwdSYCABU6EVEBKdMZUVfZDocLVJC2JwQw7HUTWPjYhQxC6hXFclZVCSJuwOZwI0AMxRTN8ZtR1i09SVYgALKHA4iQQWOGRiHe-ZQGZiQAKxQAA-CQA1VWxm1QDI9W2qJQ0jSFu3unNOxMEAA

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何使这个通用 TypeScript 函数按预期工作? 的相关文章

随机推荐