打字稿:根据另一个键的输入键入一个键

时间:2021-06-25 07:52:13

标签: typescript typescript-generics

假设我有以下界面:

interface A extends Entity {
  propA: string;
  propB: number;
  propC: boolean;
}

我可以通过以下方式构建过滤器来查询此接口背后的对象:

const filters: Filter<A>[] = [
  { op: 'eq', field: 'propA', value: 'test' },
  { op: 'in', field: 'propB', value: [1,2,3] },
  { op: 'exists', field: 'propC' }
]

op 的所有值对所有字段和字段类型都有效。但是上面的两个示例有一个额外的 value 属性。此属性对于 op 的某些值是必需的,并且取决于值,应该是单个标量或标量数组。

到目前为止我已经了解了它,但这就是我遇到的问题:value 的类型必须与我们过滤的对象的属性类型相匹配。

所以这应该是有效的:

// single value operators
const filter: Filter<A> = { op: 'eq', field: 'propA', value: 'test' };
const filter: Filter<A> = { op: 'noteq', field: 'propB', value: 5 };
const filter: Filter<A> = { op: 'eq', field: 'propC', value: true };

// multi value operators
const filter: Filter<A> = { op: 'in', field: 'propA', value: ['test1', 'test2'] };
const filter: Filter<A> = { op: 'in', field: 'propB', value: [5, 6, 7] };
const filter: Filter<A> = { op: 'notIn', field: 'propC', value: [true] };

// no value operators
const filter: Filter<A> = { op: 'exists', field: 'propC' };

但这不是:

const filter: Filter<A> = { op: 'eq', field: 'propA', value: 5 }; // A.propA is string, not number
const filter: Filter<A> = { op: 'noteq', field: 'propB', value: true }; // A.propB is number, not boolean
const filter: Filter<A> = { op: 'in', field: 'propA', value: 'test' }; // in operator expects array (of strings, in this case)
const filter: Filter<A> = { op: 'in', field: 'propA', value: [5, 6, 7] }; // A.propA is string, operator is in, so value should be an array of strings, not of numbers
const filter: Filter<A> = { op: 'exists', field: 'propC', value: true }; // exists operator does not expect a value

我该怎么做?

这是我到目前为止的代码:

export interface Entity {
  id: number;
  isActive: boolean;
}

interface ComparisonFilter {
  op: 'eq'|'noteq'|'gt'|'gte'|'lt'|'lte'|'beginsWith'|'endsWith'|'contains';
  value: ???;
}

interface NoncomparisonFilter {
  op: 'exist'|'notExist';
}

interface ListFilter {
  op: 'in'|'notIn';
  value: ???[];
}

type FieldFilter<T extends Entity> = (
  ComparisonFilter |
  NoncomparisonFilter |
  ListFilter
) & { field: keyof T };

interface GroupingFilter<T extends Entity> {
  op: 'AND'|'OR';
  items: Filter<T>[];
}

export type Filter<T extends Entity> = FieldFilter<T> | GroupingFilter<T>;

1 个答案:

答案 0 :(得分:0)

您可以创建允许类型的联合

interface A {
    propA: string;
    propB: number;
    propC: boolean;
}

export interface Entity {
    id: number;
    isActive: boolean;
}
type WithoutValue = 'exist'


interface ComparisonFilter {
    op: 'ne' | 'eq' | 'noteq' | 'gt' | 'gte' | 'lt' | 'lte' | 'beginsWith' | 'endsWith' | 'contains';

}

interface NoncomparisonFilter {
    op: WithoutValue | 'notExist';
}

interface ListFilter {
    op: 'in' | 'notIn';
}

type ApplyAll<T, Filters extends { op: string }> = {
    op: Exclude<Filters['op'], WithoutValue>;
} & Enchance<T> | { op: WithoutValue, field: keyof T }

type AllFilters =
    | ComparisonFilter
    | NoncomparisonFilter
    | ListFilter;

type FieldFilter<T> = ApplyAll<T, AllFilters>

type Values<T> = T[keyof T]

type Enchance<T> = Values<{
    [P in keyof T]: { field: P, value: T[P] | Array<T[P]> }
}>

interface GroupingFilter<T> {
    op: 'AND' | 'OR';
    items: Filter<T>[];
}

export type Filter<T> = FieldFilter<T> | GroupingFilter<T>;



/**
 * Valid
 */
// single value operators
const filter1: Filter<A> = { op: 'eq', field: 'propA', value: 'test' };
const filter2: Filter<A> = { op: 'ne', field: 'propB', value: 5 };
const filter3: Filter<A> = { op: 'eq', field: 'propC', value: true };

// multi value operators
const filter4: Filter<A> = { op: 'in', field: 'propA', value: ['test1', 'test2'] };
const filter5: Filter<A> = { op: 'in', field: 'propB', value: [5, 6, 7] };
const filter6: Filter<A> = { op: 'notIn', field: 'propC', value: [true] };

// no value operators
const filter7: Filter<A> = { op: 'exist', field: 'propC' };


// Invalid

const filter_: Filter<A> = { op: 'eq', field: 'propA', value: 5 }; // A.propA is string, not number
const filter__: Filter<A> = { op: 'ne', field: 'propB', value: true }; // A.propB is number, not boolean
const filter___: Filter<A> = { op: 'in', field: 'propA', value: 'test' }; // in operator expects array (of strings, in this case)
const filter____: Filter<A> = { op: 'in', field: 'propA', value: [5, 6, 7] }; // A.propA is string, operator is in, so value should be an array of strings, not of numbers
const filter_____: Filter<A> = { op: 'exist', field: 'propC', value: true }; // exists operator does not expect a value

Playground

您可以在我的 blog

中找到有关此模式的更多说明