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

Add an extra type property base on another property

#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;
};

我使用 types union 做这种类型,它几乎可以正常工作

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
};

Playground

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

  1. 我的解决方案也强制通过 isFilterable,即使它是 false,正如我上面提到的,我只想在它是 true
  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. 有 2 个判别属性(filterTypeisFilterable);
  2. 在这种情况下,excess property check不执行;

以上的组合证明是 TypeScript 的一个确认的设计限制(参见 this and this issues on the source repository and the question 导致提出前一个问题)。

但是你能做些什么呢? 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.

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',
  isFilterable: true,
  filterSettings: {
    filterType: 'SELECT',
    options: ['option']
  }
};

Playground

工作正常,typescript 准确地告诉我们什么时候缺少某些键以及什么时候不需要某些键。

希望对大家有所帮助