2

I have a query that I generate client-side based on user input that looks like this.

const query = {
  "or": [
    {
      "field": "username",
      "operator": "in",
      "value": [
        "jdoe",
        "jsmith"
      ]
    },
    {
      "and": [
        {
          "field": "email",
          "operator": "matches",
          "value": "/^gmail.com/"
        },
        {
          "or": [
            {
              "field": "last_sign_in",
              "operator": "lt",
              "value": 1599619454323
            },
            {
              "field": "last_sign_in",
              "operator": "gt",
              "value": 1489613454395
            }
          ]
        }
      ]
    }
  ]
}

However, in an effort to migrate this to a succinct typescript representation I am struggling with making it work quite how I want it.

I have these definitions:


type Operator = 'eq' | 'in' | 'matches' | 'lt' | 'gt';
type Condition = 'and' | 'or' | 'not';

interface SimpleQuery {
  field: string;
  operator: Operator;
  value: any;
}

interface Query {
  condition: SimpleQuery[] // here I want `condition` to come from the type Condition
// I have tried these solutions involving [{ x of y }] https://github.com/microsoft/TypeScript/issues/24220
}

Here are the errors I get from the TS compiler:

A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
Cannot find name 'key'.
'Condition' only refers to a type, but is being used as a value here.

I have tried this with

type Query = {
    [key in Condition]: SimpleQuery[];
}

with this approach typescript wants me too add all the missing conditions too.

3 Answers 3

3

I think this would be the most accurate type to describe the objects you described::

type Operator = 'eq' | 'in' | 'matches' | 'lt' | 'gt';

type UnionKeys<T> = T extends T ? keyof T : never;
type Condition = UnionKeys<OperatorExpression>;

interface FieldCondition {
  field: string;
  operator: Operator;
  value: any;
}

type BinaryExpression<T extends PropertyKey> = {
    [P in T] : [FieldCondition | OperatorExpression, FieldCondition | OperatorExpression]
}
type UnaryExpression<T extends PropertyKey> = {
    [P in T] : [FieldCondition | OperatorExpression]
}

type OperatorExpression = BinaryExpression<"and"> | BinaryExpression<"or">  | UnaryExpression<"not"> 


const query: OperatorExpression = {
  "or": [
    {
      "field": "username",
      "operator": "in",
      "value": [
        "jdoe",
        "jsmith"
      ]
    },
    {
      "and": [
        {
          "field": "email",
          "operator": "matches",
          "value": "/^gmail.com/"
        },
        {
          "or": [
            {
              "field": "last_sign_in",
              "operator": "lt",
              "value": 1599619454323
            },
            {
              "field": "last_sign_in",
              "operator": "gt",
              "value": 1489613454395
            }
          ]
        }
      ]
    }
  ]
}

Playground Link

This version enforces correct arity of logical operators (using tuple types) and derived the Condition union based on the operator union instead.

Sign up to request clarification or add additional context in comments.

1 Comment

Hi, this is a great answer because as a basic TS user I was able to learn some more advanced stuff, so thank you for that. For my use case I extended you answer to use type Expression<T extends PropertyKey> = { [P in T]: [FieldCondition | OperatorExpression][] } Because I want to be able to have more than 2 expressions that can be ANDed or ORed. However, this also allows for just a single thing to be ANDed which I am not sure how to fix.
1

I think you need to do a recursion here (playground):

type Operator = 'eq' | 'in' | 'matches' | 'lt' | 'gt';
type Condition = 'and' | 'or' | 'not';

interface SimpleQuery {
  field: string;
  operator: Operator;
  value: any;
}

type Query = {
  [key in Condition]: Array<SimpleQuery | Query>;
}

const query: Query = {
  "or": [
    {
      "field": "username",
      "operator": "in",
      "value": [
        "jdoe",
        "jsmith"
      ]
    },
    {
      "and": [
        {
          "field": "email",
          "operator": "matches",
          "value": "/^gmail.com/"
        },
        {
          "or": [
            {
              "field": "last_sign_in",
              "operator": "lt",
              "value": 1599619454323
            },
            {
              "field": "last_sign_in",
              "operator": "gt",
              "value": 1489613454395
            }
          ]
        }
      ]
    }
  ]
}

Comments

1

Is this the query interface you want? It'll work on your query example but not sure it's to the level of detail you want...

interface Query {
   [key:string]: (SimpleQuery | Query)[]; 
}

1 Comment

in this case you can use and123 instead of and etc, what isn't acceptable I guess.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.