'Assign a specific string value to first Array position

I have a complex type JoinConditionOptions (it is a SQL conditional object), the idea is to have a never ending nest of arrays, objects and strings. Those are current types and interfaces:

type JoinConditionOptions =
    | ConditionOptionsArray
    | OnConditionObject
    | string
    | undefined;

type ConditionOptionsArray = Array<JoinConditionOptions>;

type sqlValues = string | Date | Array<sqlValues> | null | boolean | number;

interface OnConditionObject {
    __or?: boolean;
    __col_relation?: ColumnRelationObject;
    [k: string]:
        | sqlValues
        | OperatorOptionsObject
        | ColumnRelationObject
        | undefined;
    [y: number]: never;
}

interface OperatorOptionsObject {
    like?: string;
    notlike?: string;
    rlike?: string;
    notrlike?: string;
    between?: Array<String | number | Date>;
    notbetween?: Array<String | number | Date>;
    in?: Array<String | number | Date>;
    notin?: Array<String | number | Date>;
    '>'?: String | number | Date;
    '<'?: String | number | Date;
    '>='?: String | number | Date;
    '<='?: String | number | Date;
    '<>'?: String | number | Date;
    '!='?: String | number | Date;
    '='?: String | number | Date;
}

interface ColumnRelationObject {
    [k: string]: string;
}

With that it is possible to do a complex nesting of Arrays, objects and strings with no problems. Example:

[
    {
        id: [1, 2, 3],
    },
    'user.id is not null',
    {
        __or: true,
        name: { like: 'john' },
        year: 2022,
    },
],

That will output:

"(`u`.`id` IN (1,2,3) AND (user.id is not null) AND (`u`.`name` LIKE 'john' OR `u`.`year` = 2022)"

Now the problem is I want to tell typescript that on the first position of any array it can be a string: "__or". I want typescript to show that it is an option when trying to build that JoinConditionOptions object.

I tried to do add "__or" to the type ConditionOptionsArray with an interface merge:

interface FirstOr {
    [0]: '__or';
}

type ConditionOptionsArray = Array<JoinConditionOptions> & FirstOr;

But unsuccessfull (causing errors when compiling the example above: "Type 'string' is not assignable to type 'never'")

Any ideas on how to achieve this and maintaining the current features of the JoinConditionsType? I am sorry if I did not make it clear enough, let me know.

Edit 1

Awsering @sno2: TypeScript Playground Link That playground contains my old code. Here the "__or" string recomendation on first positions worked perfectly fine, but then I needed to add another key option to ConditionObject: "__col_relation", this new key is included on that TypeScript Playground, but I just cant make the feature of showing a string "__or" as an option on the first position of ConditionsOptionsArray and at the same time include the new key "__col_relation" in the ConditionObject. Any Ideas on how to combine both of those type structures from those two Ts Playgrounds?



Solution 1:[1]

You can use the tuple literal type notation to have a union of "__or" along with JoinConditionOptions as the first item then have a rest item to type the rest of the items as your JoinConditionOptions type. Here is the solution:

type JoinConditionOptions =
    | ConditionOptionsArray
    | OnConditionObject
    | string
    | undefined;

type sqlValues = string | Date | Array<sqlValues> | null | boolean | number;

interface OnConditionObject {
    __or?: boolean;
    __col_relation?: ColumnRelationObject;
    [k: string]:
        | sqlValues
        | OperatorOptionsObject
        | ColumnRelationObject
        | undefined;
    [y: number]: never;
}

interface OperatorOptionsObject {
    like?: string;
    notlike?: string;
    rlike?: string;
    notrlike?: string;
    between?: Array<String | number | Date>;
    notbetween?: Array<String | number | Date>;
    in?: Array<String | number | Date>;
    notin?: Array<String | number | Date>;
    '>'?: String | number | Date;
    '<'?: String | number | Date;
    '>='?: String | number | Date;
    '<='?: String | number | Date;
    '<>'?: String | number | Date;
    '!='?: String | number | Date;
    '='?: String | number | Date;
}

interface ColumnRelationObject {
    [k: string]: string;
}

type ConditionOptionsArray = ["__or" | JoinConditionOptions, ...JoinConditionOptions[]];

const foo: ConditionOptionsArray = ["__or", { like: "foo" }];
const foo2: ConditionOptionsArray = [{ like: "foo" }];

TypeScript Playground Link

Solution 2:[2]

I managed to achieve this with the code bellow. Note that the type "String" instead of "string" was necessary to typescript recommend "__or" as an option on the first position of the Array.

export type ConditionOptions =
    | ConditionOptionsArray
    | ConditionObject
    | String
    | undefined;

interface ConditionOptionsArray extends Array<any> {
    [0]?: '__or' | ConditionOptions;
    [index: number]: ConditionOptions;
}

type sqlValues = String | Date | Array<sqlValues> | null | boolean | number;

interface ConditionObject {
    __or?: boolean;
    __col_relation?: ColumnRelationObject;
    [k: string]:
        | sqlValues
        | OperatorOptionsObject
        | ColumnRelationObject
        | undefined;
    [y: number]: never;
}

interface ColumnRelationObject {
    [k: string]: String;
}
export const isColumnRelationObject = (
    value: any
): value is ColumnRelationObject =>
    value !== undefined &&
    value !== null &&
    !Array.isArray(value) &&
    typeof value === 'object' &&
    Object.entries(value).reduce(
        (acc: boolean, [key, val]) =>
            acc && typeof key === 'string' && typeof val === 'string',
        true
    );

export interface OperatorOptionsObject {
    like?: String;
    notlike?: String;
    rlike?: String;
    notrlike?: String;
    between?: Array<String | number | Date>;
    notbetween?: Array<String | number | Date>;
    in?: Array<String | number | Date>;
    notin?: Array<String | number | Date>;
    '>'?: String | number | Date;
    '<'?: String | number | Date;
    '>='?: String | number | Date;
    '<='?: String | number | Date;
    '<>'?: String | number | Date;
    '!='?: String | number | Date;
    '='?: String | number | Date;
}
const OperatorOptionsObjectKeys: Array<String> = [
    'like',
    'notlike',
    'rlike',
    'notrlike',
    'between',
    'notbetween',
    'in',
    'notin',
    '>',
    '<',
    '>=',
    '<=',
    '<>',
    '!=',
    '=',
];
export const isOperatorOptionsObject = (
    val: any
): val is OperatorOptionsObject => {
    return (
        typeof val === 'object' &&
        val !== null &&
        val !== undefined &&
        Object.keys(val).reduce(
            (prev: boolean, cur: String) =>
                prev || OperatorOptionsObjectKeys.includes(cur),
            false
        )
    );
};

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 sno2
Solution 2 Gabriel Cunha