TypeScript - 从泛型联合类型缩小类型


//Type declaration:
interface TickListFilter {
type: "tickList";
value: string;
}
interface ColorFilter {
type: "color"
value: ColorValueType
}
type Filter = TickListFilter | ColorFilter;

...
onValueChange = (filter: Filter, newValue: Filter["value"]) => {
if (filter.type === "tickList") {
// variable 'filter' has TickListFilter type (OK)
// variable 'filter.value' has string type (OK)
// variable newValue has ColorValueType | string. I know why, let's fix it!
}
...
onValueChange = <T extends Filter>(filter: T, newValue: T["value"]) => {
if (filter.type === "tickList") {
// variable 'filter' has Filter type (wrong... i need tick list)
// variable 'filter.value' has string | ColorValueType type (wrong, it should be string)
// variable newValue has ColorValueType | string.
}

我该如何解决这个问题?我知道为什么会发生这种情况,因为 TS 无法按泛型类型区分联合。但是,是否有任何解决方法可以按照我的描述工作?

我想使用与开关盒类似的结构(不仅仅是如果-否则(

啊,欢迎来到microsoft/TypeScript#13995和microsoft/TypeScript#24085。 目前,编译器不使用控制流分析来缩小泛型类型参数或依赖于泛型类型参数的类型值。 从技术上讲,缩小类型参数本身是错误的,因为有人可能会以这种方式调用泛型版本:

const filter: Filter = Math.random() < 0.5 ?
{ type: "tickList", value: "str" } : { type: "color", value: colorValue };
const newValue: string | ColorValueType = Math.random() < 0.5 ? "str" : colorValue;
onValueChange(filter, newValue); // no compiler error
// const onValueChange: <Filter>(filter: Filter, newValue: string | ColorValueType) => void

编译器将推断FilterforT,根据通用签名,这是"正确的",但它会导致与非通用版本相同的问题。 在函数内部可能是newValue不匹配filter. 😢


在上述 GitHub 问题中,关于如何处理这个问题,已经提出了许多建议和讨论。 如果像 microsoft/TypeScript#27808 这样的东西,你可以说T extends-exactly-one-member--of Filter这样的话,意思是T可以是TickListFilterColorFilter,但不能Filter。 但是现在没有办法说这个。


现在,最简单的方法(假设您不想实际检查函数的实现内部filternewValue相互匹配(将涉及类型断言或类似的东西。 您需要放松类型检查以允许代码,并且只是期望/希望没有人会像我上面那样调用您的函数。

下面是使用类型断言的一种方法:

const onValueChange = <T extends Filter>(filter: T, newValue: T["value"]) => {
const f: Filter = filter; // widen to concrete type
if (filter.type === "tickList") {
const v = newValue as TickListFilter["value"]; // technically unsafe narrowing
f.value = v; // okay
} else {
const v = newValue as ColorFilter["value"]; // technically unsafe narrowing
f.value = v; // okay
}
}

操场链接到代码

如果你想要一个手动解决方案,你可以求助于编写自己的类型保护:

// Either like this, simple but a lot of typing
const isColorFilter = (value: Filter): value is ColorFilter => value.type === 'color';
// Or write a small type guard generator if there are lots of these types
const createFilterTypeGuard = <T extends Filter>(type: T['type']) => (value: Filter): value is T => value.type === type;
// And then
const isColorFilter = createFilterTypeGuard<ColorFilter>('color');

然后在您的代码中,您可以像这样使用它们:

if (isColorFilter(filter)) {
// filter is ColorFilter
}

但是,您将遇到newValue问题 .由于类型防护只会缩小filter的类型,newValue将保持不缩小:

onValueChange = <T extends Filter>(filter: T, newValue: T["value"]) => {
// OK, filter is T and newValue is T['value']
filter.value = newValue;
if (isColorFilter(filter)) {
// Not OK, filter is ColorFilter now but newValue is still Filter['value']
filter.value = newValue;
}
}

调用onValueChange时您仍然会得到正确的验证,您只需要在函数体内进行一些类型转换:(

最新更新