deepkit / deepkit-framework

A new full-featured and high-performance TypeScript framework
https://deepkit.io/
MIT License
3.18k stars 121 forks source link

[type] Issues with assert/cast combined with complex generic types #493

Closed alpharder closed 10 months ago

alpharder commented 10 months ago

I've managed to reduce reproduction code to this:

import * as nodeAssert from 'node:assert/strict';
import { assert, cast } from '@deepkit/type';

type List<A = never> = ReadonlyArray<A>;

type BuiltIn =
  // eslint-disable-next-line @typescript-eslint/ban-types
  | Function
  | Error
  | Date
  | {
      readonly [Symbol.toStringTag]: string;
    }
  | RegExp
  | Generator;

type OnlyUserKeys<O> = O extends List
  ? keyof O & number
  : O extends BuiltIn
  ? never
  : keyof O & (string | number);

type NestedKeyOf<ObjectType extends object> = {
  [Key in OnlyUserKeys<ObjectType>]: ObjectType[Key] extends object
    ? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
    : `${Key}`;
}[OnlyUserKeys<ObjectType>];

/**
 * It's JSON:API spec compatible, without any particular reason.
 * Please note that JSON:API spec compatibility is not the main goal of this library,
 * so don't expect everything to be compatible with JSON:API.
 */
export type ResourceQuerySort<O extends object> = (
  | `-${NestedKeyOf<O>}`
  | `${NestedKeyOf<O>}`
)[];

export type ResourceQueryFilterOperatorsWithNestedQueries<
  TValue extends object,
> = {
  $not?: ResourceQueryFilter<TValue>;
  $or?: ResourceQueryFilter<TValue>[];
  $nor?: ResourceQueryFilter<TValue>[];
  $and?: ResourceQueryFilter<TValue>[];
};

export type ResourceQueryFilterOperators<TValue> = {
  $eq?: TValue;
  $ne?: TValue;
  $lt?: TValue;
  $gt?: TValue;
  $lte?: TValue;
  $gte?: TValue;
  $in?: TValue[];
  $nin?: TValue[];
  $all?: TValue[];
};

export type ResourceQueryFiltersSubsetBasedOnNestedQueryOperators<
  TResourceObject extends object,
> = ResourceQueryFilterOperatorsWithNestedQueries<TResourceObject>;

export type ResourceQueryFiltersSubsetBasedOnResourceObjectKeys<
  TResourceObject extends object,
  TResourceObjectUserKeys extends
    keyof TResourceObject = OnlyUserKeys<TResourceObject>,
> = {
  [TKey in TResourceObjectUserKeys]?: TResourceObject[TKey] extends object
    ? ResourceQueryFilter<TResourceObject[TKey]> &
        ResourceQueryFilterOperators<TResourceObject[TKey]> &
        ResourceQueryFilterOperatorsWithNestedQueries<TResourceObject[TKey]>
    :
        | TResourceObject[TKey]
        | ResourceQueryFilterOperators<TResourceObject[TKey]>;
};

export type ResourceQueryFilter<TResourceObject extends object> =
  ResourceQueryFiltersSubsetBasedOnResourceObjectKeys<TResourceObject> &
    ResourceQueryFiltersSubsetBasedOnNestedQueryOperators<TResourceObject>;

export type ResourceQuery<T extends object> = {
  filter?: ResourceQueryFilter<T>;
  sort?: ResourceQuerySort<T>;
  limit?: number;
  offset?: number;
};

type MyQuery = ResourceQuery<{
  k: string;
  t: number;
  Y: {
    yyf: string;
  };
}>;

const validMyQuery: MyQuery = {
  filter: {
    k: 'k',
    t: 44,

    $or: [
      { t: { $gte: 32 } },
      {
        Y: {
          yyf: { $ne: 'ne' },
        },
      },
    ],
  },

  sort: ['-k', 't', '-Y.yyf'],
};

/**
 *  "validMyQuery" is a perfectly valid instance of MyQuery type, but this assertion fails due to incorrect casting
 *    expected: {
 *     filter: {
 *       k: 'k',
 *       t: 44,
 *       '$or': [ { t: { '$gte': 32 } }, { Y: { yyf: [Object] } } ]
 *     },
 *     sort: [ '-k', 't' ]
 *   },
 *   actual: { filter: { '$or': [ {}, {} ] }, sort: [ undefined, undefined ] },
 */
nodeAssert.deepEqual(cast<MyQuery>(validMyQuery), validMyQuery);

const invalidMyQuery: MyQuery = {
  filter: {
    // @ts-expect-error Because t should only accept numbers or  objects
    t: 'str',
    Y: {
      yyf: {
        // @ts-expect-error because $ne should only accept string here
        $ne: {
          IMPOSSIBLE: {
            IMPOSSIBLE: [],
          },
        },
      },
    },

    // cast should remove these, and it does
    NON_EXISTENT: {
      NON_EXISTENT: {},
    },
  },

  // @ts-expect-error because "NON_EXISTENT" is invalid property path
  sort: ['-NON_EXISTENT'],
};

// This should throw, but it doesn't
console.log(assert<MyQuery>(invalidMyQuery));
alpharder commented 10 months ago

https://github.com/deepkit/deepkit-framework/commit/fde795ee6998606b0791f936a25ee85921c6586a fixes the issue for me