我正在尝试定义一个与 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]))
}
}
但这不是我基于这个问题的真实代码的结构。