添加基于另一个属性的额外类型属性

问题描述

#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),只有当 Columnoptions 时,filterType 才需要 'SELECT'。我试图用类型联合来做到这一点,但它变得很奇怪:
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
};

Playground

如果我将 isFilterable 设置为 false,TS 不会建议不需要的属性(很好),但如果我传递这些不需要的道具也不会显示错误(不好)

  1. 我的解决方案也强制传递 isFilterable,即使它是 false,正如我上面提到的,我只想在它是 true 时传递它

有没有办法改进我的解决方案(或其他解决方案)以实现我在开头所描述的内容 (#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. 有 2 个判别属性(filterTypeisFilterable);
  2. 在这种情况下,不执行excess property check

以上的组合被证明是 TypeScript 的一个确认的设计限制(请参阅源存储库上的 thisthis 问题以及导致前一个问题被提出的 the question ).

但是你能做些什么呢? never 来拯救:被禁止作为属性与具有类型 never 几乎相同,因此对 isFilterable 属性进行相应的简单更改以避免使 isFilterable第二个判别属性(为简单起见省略了额外的可选属性)应该可以解决问题:

type Column = 
(
  { name: string,isFilterable:true,filterType:"SELECT",options:string[] } | 
  { name: string,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',options: [] }; //OK
const textFilter: Column = { name: "col2",filterType: "TEXT" }; //OK

Playground

,

好的,经过几个晚上我设法做到了,我有两个解决方案:

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'],};

Playground

它如我所愿,出现了 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',filterSettings: {
    filterType: 'SELECT',options: ['option']
  }
};

Playground

工作正常,打字稿会准确地告诉我们何时遗漏了某个键以及何时不需要某个键。

希望对大家有所帮助