在另一属性的基础上添加额外的类型属性

5

#1 我有一个列的类型是对象。列可以过滤或不过滤,如果isFilterabletrue,那么Column类型应该要求:filterTypeisTopBarFilter?options(但仅当filterType'SELECT'时 - #2)。

type Column = {
  name: string;
  isFilterable: boolean; // passing here false should be equal with not passing the property at all (if possible)

  // below properties should exist in type only if isFilterable = true
  filterType: 'SELECT' | 'TEXT' | 'DATE';
  options: string[]; // this property should exist in type only if filterType = 'SELECT'
  isTopBarFilter?: boolean;
};

我使用类型联合来实现这种类型,它几乎可以正常工作。

type FilterableColumn = {
  isFilterable: true;
  filterType: 'SELECT' | 'TEXT' | 'DATE';
  options: string[];
  isTopBarFilter?: boolean;
};

type NonFilterableColumn = {
  isFilterable: false;
};

type Column = (NonFilterableColumn | FilterableColumn) & {
  name: string;
};

但是:

  1. 正如我之前提到的 (#2),如果 filterType'SELECT'Column 应该仅需要 options。我尝试使用类型联合来实现这一点,但结果变得奇怪:
type FilterableSelectColumn = {
  filterType: 'SELECT';
  options: string[];
};

type FilterableNonSelectColumn = {
  filterType: 'TEXT' | 'DATE' | 'NUMBER';
};

type FilterableColumn = (FilterableSelectColumn | FilterableNonSelectColumn) & {
  isFilterable: true;
  isTopBarFilter?: boolean;
};

type NonFilterableColumn = {
  isFilterable: false;
};

type Column = (FilterableColumn | NonFilterableColumn) & {
  name: string;
};

// e.g
const col: Column = {
  name: 'col2',
  isFilterable: false,
  filterType: 'SELECT', // unwanted
  isTopBarFilter: false, // unwanted
  options: ['option1'], // unwanted
};

操场

如果我将isFilterable设置为false,TS不会建议不需要的属性(这很好),但是如果我传递这些不需要的props,它也不会显示错误(这很糟糕)。
我的解决方案还强制传递isFilterable,即使它是false,正如我上面提到的,我只想在它是true时传递它。
有没有办法改进我的解决方案(或另一个解决方案),以实现我在开头描述的内容(#1)?

2
哦,好主意。你刚好机缘巧合地发现了交集与独立联合体的行为。如果没有其他人赶上我,明天我会准备一份草案来回答这个问题。这里是一个关于你的想法的证明,仅使用嵌套独立联合体就可以完美运作:https://tsplay.dev/WzyJRm - Oleg Valter is with Ukraine
1
@OlegValter,感谢您的回复。我一直在尝试根据您上面发送的代码片段来改进我的代码,但它并不完全适合,我无法处理它。如果您能帮助我解决我的问题,我将不胜感激。 - bastej
哎呀,看来我被其他事情分散了注意力,很抱歉 - 谢谢你回复提醒我 :( 我会尽快尝试解决。不过我需要先查找一个好的参考资料 - 我记得在 TS 源代码仓库上有关于类似问题的讨论。 - Oleg Valter is with Ukraine
1
抱歉,我删除了上面的评论,因为随着时间的推移,我忘记了你的问题不是关于嵌套判别联合,而是关于对象类型的并集与交集。这是一个单独的问题(可能与问题#9919有关,但我需要先进行调查)。 - Oleg Valter is with Ukraine
终于搞定了 :) 问题最终是不同的,但是可以解决。对于编辑和随后的回滚,我表示歉意,我需要运行一个小实验,请不要介意 - 谢谢! - Oleg Valter is with Ukraine
2个回答

1

让我们看看分配律如何影响在您的情况下交集和并集的解决方式。首先,以下内容:

type OuterUnionMemberA = (UnionMemberA | UnionMemberB) & IntersectedA;

等同于这个:

type OuterUnionMemberA = (UnionMemberA & IntersectedA) | (UnionMemberB & IntersectedA);

这导致了以下结果:
type ComplexType = (OuterUnionMemberA | IntersectedB) & OuterIntersected;

等价于这个复合联合体:
type ComplexType = (UnionMemberA & IntersectedA & OuterIntersected) | (UnionMemberB & IntersectedA & OuterIntersected) | (IntersectedB & OuterIntersected);

让我们手动解析别名,看看它留下了什么:

type ComplexType = { 
  filterType: 'SELECT'; 
  options: string[];
  isFilterable: true; 
  extraProp?: boolean;
  name: string;
} | {
  filterType: 'TEXT' | 'DATE' | 'NUMBER';
  isFilterable: true; 
  extraProp?: boolean;
  name: string;
} | {
  isFilterable: false;
  name: string
}

为了验证我们的期望是相同类型,让我们进行一个等式测试:
type isSupertype = ComplexType extends ComplexTypeUnwrapped ? true : false; //true
type isSubtype = ComplexTypeUnwrapped extends ComplexType ? true : false; //true

所有上述工作都是为了让以下内容更加清晰明了:
  1. 有两个判别属性(filterTypeisFilterable);
  2. 在这种情况下,多余属性检查 不会被执行;
以上的组合结果是 TypeScript 的一个已确认的设计限制(请参阅源代码库中 此问题此问题 以及导致前者问题提出的 问题)。

但是你能做些什么呢?never来拯救:禁止作为属性与有一个类型never基本相同,因此简单地更改isFilterable属性,以避免使isFilterable成为第二个辨别属性(为简单起见省略了额外的可选属性)就可以解决问题:

type Column = 
(
  { name: string, isFilterable:true,filterType:"SELECT",options:string[] } | 
  { name: string, isFilterable:true,filterType:"TEXT"|"DATE" } | 
  { name: string, isFilterable:never } |
  { name: string, isFilterable:false } //allows "name-only" case
)

const notFilterableAll: Column = { name: 'col2', isFilterable:false };
const notFilterableText: Column = { name: 'col2', filterType: "TEXT" }; //Property 'isFilterable' is missing;
const notFilterableSelect: Column = { name: "col2", filterType: "SELECT", options: [] }; //Property 'isFilterable' is missing;
const notFilterableSelectMissingOpts: Column = { name: "col2", filterType: "SELECT" }; //Type '"SELECT"' is not assignable to type '"TEXT" | "DATE"';
const selectFilterOk: Column = { name: 'col2', isFilterable:true, filterType: "SELECT", options: [] }; //OK
const textFilter: Column = { name: "col2", isFilterable:true, filterType: "TEXT" }; //OK

Playground

可以直接翻译为:

{{链接1:游乐场}}

,其中保留了HTML标签和特殊符号。

感谢您详尽的回答。使用“never”的想法涵盖了大多数情况,但在{ name: 'col2' }这种情况下,TS仍然需要isFilterable,但不应该:/ - bastej
@bastej - 刚刚注意到我打错了一个字,让读者猜测我指的是哪个法律 :) 关于 { name: string } 的情况 - 是的,不幸的是,这是解决方案需要付出的代价 - 我认为现在你无法完全解决它。至少团队正在跟进... - Oleg Valter is with Ukraine
@bastej - 你可以像这样在Column类型中添加第四个联合成员:{ name: string, isFilterable:false },至少让你有能力将属性作为false传递,就像你最初的解决方案一样(我们只使用never技巧来打破第二个辨别标志),但仅此而已。 - Oleg Valter is with Ukraine
我希望这至少比在不需要的属性上没有错误要好 :) - Oleg Valter is with Ukraine

0

好的,在经过几个晚上的努力后,我终于做到了,我有两个解决方案:

1.

type FilterableColumn = {
  isFilterable: true;
  isTopBarFilter?: boolean;
} & (
  | {
      filterType: 'SELECT';
      options: string[];
    }
  | {
      filterType: 'TEXT' | 'DATE';
    });

type NonFilterableColumn = {
  isFilterable?: undefined; // same result with never
  filterType?: undefined; // same result with never
};

type ColumnBaseFields = {
  name: string;
};

type Column = (FilterableColumn | NonFilterableColumn) & ColumnBaseFields;

const column: Column = {
  name: 'someName',
  isFilterable: true,
  filterType: 'SELECT',
  options: ['option'],
};

游乐场

它按照我想要的方式工作,但在某些情况下会出现Typescript错误,而错误描述不准确。我注意到TypeScript在同一嵌套级别上使用多个联合时的行为很奇怪

因此,我想出了第二种解决方案,使用嵌套过滤器选项

2.

type FilterSettings = (
  | {
      filterType: 'SELECT';
      options: string[];
    }
  | {
      filterType: 'TEXT';
    }) & {
  isTopBarFilter?: boolean;
};

type FilterableColumn = {
  isFilterable: true;
  filterSettings: FilterSettings;
};

type NonFilterableColumn = {
  isFilterable?: undefined; // same result with never
};

type ColumnBaseFields = {
  name: string;
};

type Column = (FilterableColumn | NonFilterableColumn) & ColumnBaseFields;

const column: Column = {
  name: 'someName',
  isFilterable: true,
  filterSettings: {
    filterType: 'SELECT',
    options: ['option']
  }
};

游乐场

很好,TypeScript 可以准确地告诉我们哪些键值缺失或不需要。

希望对某些人有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接