这个错误困扰着我多年。和thanks to Inigo I finally understood the problem。
tl; dr:当您在通用声明中使用T extends A
时,您需要T
至少为 A
,但是它可以具有更多的属性(通过是不同的子类型)。
当您要创建一个符合T
的对象时,就会出现问题。您不能,因为T
可能需要比A
的其他或更具体的属性。
当我们创建处理通用对象和数组的React Hooks时,这种情况发生了很多。
一个解决方案是将回调函数传递给知道如何将A
之类的内容转换为T
的通用,鉴于您的通用功能期望处理的内容。
代码
假设我们正在为combobox创建一个钩子的抽象过滤选项,如果没有搜索结果:
type SelectOption = {
name: string;
value: string | null;
icon?: string;
description?: string;
disabled?: boolean;
};
function useComboboxFilter<T extends SelectOption>(props: {
options: T[];
filterValue: string;
}): { filteredOptions: T[] } {
let filteredOptions = props.filterValue
? props.options.filter((option) => option.name.includes(props.filterValue))
: props.options;
if (props.filterValue && !filteredOptions.length) {
filteredOptions = [
{ name: "No results found", value: null, disabled: true },
];
}
return { filteredOptions };
}
react devs的注释
因为这是一个示例,所以我没有正确优化代码。适当的react钩子应包裹filteredOptions
和返回对象:
function useComboboxFilter<T extends SelectOption>(props: {
options: T[];
filterValue: string;
}): { filteredOptions: T[] } {
const filteredOptions = useMemo(() => {
let filteredOptions = props.filterValue
? props.options.filter((option) =>
option.name.includes(props.filterValue)
)
: props.options;
if (props.filterValue && !filteredOptions.length) {
filteredOptions = [
{ name: "No results found", value: null, disabled: true },
];
}
return filteredOptions;
}, [props.filterValue, props.options]);
return useMemo(() => ({ filteredOptions }), [filteredOptions]);
}
问题
如果您check this code with the TypeScript compiler,您将获得此错误:
Type '{ name: string; value: null; disabled: true; }'
is not assignable to type 'T'.
'{ name: string; value: null; disabled: true; }' is
assignable to the constraint of type 'T', but 'T' could
be instantiated with a different subtype of constraint
'SelectOption'. ts(2322)
这是一个隐秘的错误消息:它在不帮助读者了解情况或实际上做错的事情的情况下正确地描述了问题。
这里的问题很简单:{ name: string; value: null; disabled: true; }
满足SelectOption
,但是由于T
可能是SelectOption
的不同子类型,也就是说,一种具有略有不同需求的类型,因此您的通用函数不知道 create create create create>它的新版本。
我们示例的一个明显问题是:想象我们创建的通用类型T
需要一个id
属性:
type SelectOptionWithId = {
id: string;
} & SelectOption;
const myFilter = useComboboxFilter<SelectOptionWithId>({
options: [],
filterValue: "bla",
});
在这种情况下,myFilter
的结果将具有一个没有id
的对象,因为通用挂钩不知道它需要添加该属性。
由于通用代码不知道T
,因此您只能创建一个新版本的SelectOption
。为了解决此问题,您需要一种将SelectOption
对象转换为T
的方法,只有您的非生成代码才能提供。
一个解决方案
因为在useComboboxFilter
中您只知道SelectOption
,而您只关心它的name
,value
和disabled
属性,因此您可以创建一个必需的函数参数,将部分SelectOption
转换为任何t:
function useComboboxFilter<T extends SelectOption>(props: {
options: T[];
createOption: (
option: Pick<SelectOption, "name" | "value" | "disabled">
) => T;
filterValue: string;
}): { filteredOptions: T[] } {
let filteredOptions = props.filterValue
? props.options.filter((option) => option.name.includes(props.filterValue))
: props.options;
if (props.filterValue && !filteredOptions.length) {
filteredOptions = [
props.createOption({
name: "No results found",
value: null,
disabled: true,
}),
];
}
return { filteredOptions };
}
然后,当您使用SelectOption
子类型的通用useComboboxFilter
时,您必须确保通用函数可以创建一个满足该类型的新对象:
type SelectOptionWithId = {
id: string;
} & SelectOption;
const createOption = (option: SelectOption): SelectOptionWithId => ({
id: Math.random().toString(),
...option,
});
const myFilter = useComboboxFilter({
options: [],
createOption,
filterValue: "bla",
});
请注意,使用createOption
属性,typeScript Infers SelectOptionWithId
for myFilter
。
如果您想了解有关仿制药和其他整洁的打字稿功能的更多信息,请务必检查@mattpocockuk的YouTube videos和他的Total TypeScript online course。