在香草JS中,我能够编写一些类似于以下内容的代码:
function renderTextField(props) { }
function renderSelectField(props) { }
const fieldMapping = {
text: renderTextField,
select: renderSelectField,
};
function renderField(field) {
const renderFn = fieldMapping[field.type];
renderFn(field.data);
}
仅使用两种类型的字段,以使示例保持较小,但是此代码的好处是,通用方法不需要了解字段的类型,并且将决策委托给{ {1}}。
我正在尝试在fieldMapping
中编写类似内容。但是我无法弄清楚如何使类型起作用,并且仍然使用对象在TypeScript
和要委派的函数之间提供映射。
我意识到我可以使用switch语句或条件语句代替对象来映射事物,但我更愿意以这种方式进行操作。
type
答案 0 :(得分:3)
恐怕您将不得不使用type assertion来避免此处的代码重复。 TypeScript的类型系统只是没有对这些"correlated record types"的良好支持,也没有依赖于两个联合类型的值(联合不是独立的)相互作用的任何操作。
您已经到达switch
中的冗余代码解决方法;这是不安全声明的解决方法:
function assertNarrowFunction<F extends (arg: any) => any>(f: F) {
return f as (arg: Parameters<F>[0]) => ReturnType<F>; // assert
}
该函数采用联合类型的函数,例如((a: string)=>number) | ((a: number)=>boolean)
,并且不安全地将其缩小 到一个函数,该函数采用其参数类型的联合并返回其返回类型的联合,例如{ {1}}。这是不安全的,因为前一个联合类型的函数可能类似于((a: string | number) => string | number)
,但肯定不匹配const f = Math.random()<0.5 ? ((a: string)=>a.length) : ((a: number)=>number.toFixed())
。我无法安全地调用((a: string | number) => string | number)
,因为也许f(5)
是字符串长度的函数。
无论如何,您可以在f
上使用这种不安全的缩小来使错误消失:
renderFn
您对function renderFnAssertion(field: FormField) {
const renderFn = assertNarrowFunction(fieldMapping[field.type]);
renderFn(field.data); // okay
}
的类型撒谎了……对它的接受并不是太多,它可以接受任何旧参数(例如,renderFn
将根据需要失败),但是足够了它会允许这样做:
renderFn(123)
因此,您必须小心。
好的,希望能有所帮助;祝你好运!
答案 1 :(得分:2)
这种方法的好处是,如果我们删除所有类型信息,那么您的问题中将保留原始的原始JavaScript。我看到的唯一缺点是as
可以解决jcalz
指出的缺乏相关记录类型的问题。在这种情况下,这看起来不错,因为我们确实比编译器知道更多,并且我们不会丢失任何类型安全性。
type TextFieldData = { value: string }
type TextField = { type: 'text', data: TextFieldData }
type SelectFieldData = { options: string[], selectedValue: string }
type SelectField = { type: 'select', data: SelectFieldData }
type FormField = TextField | SelectField
function renderTextField(props: TextFieldData) { }
function renderSelectField(props: SelectFieldData) { }
const fieldMapping = {
text: renderTextField,
select: renderSelectField,
}
// This is the new block of code.
type FindByType<Union, Type> = Union extends { type: Type } ? Union : never;
type TParam<Type> = FindByType<FormField, Type>['data'];
type TFunction<Type> = (props: TParam<Type>) => void;
function renderFieldDoesNotWork(field: FormField) {
// This is the cast that seems unavoidable without correlated record types.
const renderFn = fieldMapping[field.type] as TFunction<typeof field.type>;
renderFn(field.data)
}
在您提出的问题的代码中,如果开发人员在映射中犯了这样的错误,则编译器不会抱怨:
const fieldMappingOops = {
select: renderTextField, // no compiler error
text: renderTextField,
}
我们可以添加一个新类型,该类型将告诉编译器在这种情况下抱怨:
type FieldMapping = {
[Key in FormField['type']]: TFunction<Key>;
}
const fieldMappingOops: FieldMapping = {
select: renderTextField, // compiler error
text: renderTextField,
}
This GitHub comment启发了这种方法。当我们将FindByType
和switch
语句与tagged union / discriminated union类型结合使用时,if
为我们提供了一些缩小类型的能力。
在给定相关输入后,与该注释相关的类型将如何扩展:
// type f1 = {
// type: "text";
// data: TextFieldData;
// }
type f1 = FindByType<FormField, 'text'>;
// type f2 = {
// value: string;
// }
type f2 = TParam<'text'>;
// type f3 = (props: TextFieldData) => void
type f3 = TFunction<'text'>;
// type f4 = TextField | SelectField
type f4 = FindByType<FormField, 'text' | 'select'>;
// type f5 = TextFieldData | SelectFieldData
type f5 = TParam<'text' | 'select'>;
// type f6 = (props: TextFieldData | SelectFieldData) => void
type f6 = TFunction<'text' | 'select'>;