TypeScript: Typning av rekursiv typ


Jag har en fråga som jag genererar på klientsidan baserat på användarinmatning som ser ut så här.

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

Men i ett försök att migrera detta till en kortfattad TypeScript-representation har jag svårt att få det att fungera precis som jag vill.

Jag har dessa definitioner:

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
}

Här är de fel jag får från TS-kompilatorn:

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.

Jag har försökt detta med

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

Med detta tillvägagångssätt vill TypeScript att jag också lägger till alla saknade villkor.

Svar #1

Jag tror att detta skulle vara den mest exakta typen för att beskriva de objekt du beskrev:

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

Lekplatslänk

Denna version upprätthåller korrekt aritet för logiska operatorer (med hjälp av tuplatyper) och härledde Condition-unionen baserat på operatorunionen istället.

Svar #2

Jag tror att du behöver göra en rekursion här (lekplats):

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