samchon / typia

Super-fast/easy runtime validators and serializers via transformation
https://typia.io/
MIT License
4.47k stars 154 forks source link

Avoid pruning non-primitive object keys #660

Closed jasperlin451 closed 1 year ago

jasperlin451 commented 1 year ago

Feature Request

Thanks for all the help. Wish this library was around when we started the repo.

I'm hoping to use prune with some non primitive types.

Here's an example:

import { Interval } from 'luxon';
import typia from 'typia';

export const isPrune = typia.createPrune<{
  test: Interval;
}>();

The generated check seems reasonable and just checks if test is typeof object since it doesn't/can't understand the external Interval type.

export const isExample = (input: any): input is {
  test: Interval;
} => {
    const $io0 = (input: any): boolean => "object" === typeof input.test && null !== input.test && true;
    return "object" === typeof input && null !== input && $io0(input);
};

For the purposes of what were trying to accomplish, this seems good enough.

However, the prune version of the code adds:

    const $po1 = (input: any): any => {
        for (const key of Object.keys(input))
            delete input[key];
    };

This ends up destroying the Interval object. Would it be possible to not run prune out the keys in this case?

samchon commented 1 year ago

Looking at definition of @types/luxon, the Interval class does not have any property.

Therefore, removing every properties by typia.prune() is exact behavior.

I think it would better not to calling the typia.purne() function for the Interval class type.

samchon commented 1 year ago

Here the definition of Interval

/**
 * An Interval object represents a half-open interval of time, where each endpoint is a {@link DateTime}.
 * Conceptually, it is a container for those two endpoints, accompanied by methods for
 * creating, parsing, interrogating, comparing, transforming, and formatting them.
 *
 * Here is a brief overview of the most commonly used methods and getters in Interval:
 *
 * * **Creation** To create an Interval, use {@link Interval.fromDateTimes}, {@link Interval.after}, {@link Interval.before}, or {@link Interval.fromISO}.
 * * **Accessors** Use {@link Interval#start} and {@link Interval#end} to get the start and end.
 * * **Interrogation** To analyze the Interval, use {@link Interval#count}, {@link Interval#length}, {@link Interval#hasSame},
 * * {@link Interval#contains}, {@link Interval#isAfter}, or {@link Interval#isBefore}.
 * * **Transformation** To create other Intervals out of this one, use {@link Interval#set}, {@link Interval#splitAt}, {@link Interval#splitBy}, {@link Interval#divideEqually},
 * * {@link Interval.merge}, {@link Interval.xor}, {@link Interval#union}, {@link Interval#intersection}, or {@link Interval#difference}.
 * * **Comparison** To compare this Interval to another one, use {@link Interval#equals}, {@link Interval#overlaps}, {@link Interval#abutsStart}, {@link Interval#abutsEnd}, {@link Interval#engulfs}
 * * **Output** To convert the Interval into other representations, see {@link Interval#toString}, {@link Interval#toLocaleString}, {@link Interval#toISO}, {@link Interval#toISODate},
 * * {@link Interval#toISOTime}, {@link Interval#toFormat}, and {@link Interval#toDuration}.
 */
export class Interval {
    /**
     * Create an invalid Interval.
     *
     * @param reason - simple string of why this Interval is invalid. Should not contain parameters or anything else data-dependent
     * @param explanation - longer explanation, may include parameters and other useful debugging information.
     */
    static invalid(reason: string, explanation?: string): Interval;

    /**
     * Create an Interval from a start DateTime and an end DateTime. Inclusive of the start but not the end.
     *
     * @param start
     * @param end
     */
    static fromDateTimes(start: DateInput, end: DateInput): Interval;

    /**
     * Create an Interval from a start DateTime and a Duration to extend to.
     *
     * @param start
     * @param duration - the length of the Interval.
     */
    static after(start: DateInput, duration: DurationLike): Interval;

    /**
     * Create an Interval from an end DateTime and a Duration to extend backwards to.
     *
     * @param end
     * @param duration - the length of the Interval.
     */
    static before(end: DateInput, duration: DurationLike): Interval;

    /**
     * Create an Interval from an ISO 8601 string.
     * Accepts `<start>/<end>`, `<start>/<duration>`, and `<duration>/<end>` formats.
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
     *
     * @param text - the ISO string to parse
     * @param opts - options to pass {@link DateTime.fromISO} and optionally {@link Duration.fromISO}
     */
    static fromISO(text: string, opts?: DateTimeOptions): Interval;

    /**
     * Check if an object is an Interval. Works across context boundaries
     *
     * @param o
     */
    static isInterval(o: unknown): o is Interval;

    private constructor(config: unknown);

    /**
     * Returns the start of the Interval
     */
    get start(): DateTime | IfInvalid<null>;

    /**
     * Returns the end of the Interval
     */
    get end(): DateTime | IfInvalid<null>;

    /**
     * Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'.
     */
    get isValid(): boolean;

    /**
     * Returns an error code if this Interval is invalid, or null if the Interval is valid
     */
    get invalidReason(): string | null;

    /**
     * Returns an explanation of why this Interval became invalid, or null if the Interval is valid
     */
    get invalidExplanation(): string | null;

    /**
     * Returns the length of the Interval in the specified unit.
     *
     * @param unit - the unit (such as 'hours' or 'days') to return the length in.
     */
    length(unit?: DurationUnit): number | IfInvalid<typeof NaN>;

    /**
     * Returns the count of minutes, hours, days, months, or years included in the Interval, even in part.
     * Unlike {@link Interval#length} this counts sections of the calendar, not periods of time, e.g. specifying 'day'
     * asks 'what dates are included in this interval?', not 'how many days long is this interval?'
     *
     * @param unit - the unit of time to count. Defaults to 'milliseconds'.
     */
    count(unit?: DurationUnit): number | IfInvalid<typeof NaN>;

    /**
     * Returns whether this Interval's start and end are both in the same unit of time
     *
     * @param unit - the unit of time to check sameness on
     */
    hasSame(unit: DurationUnit): boolean | IfInvalid<false>;

    /**
     * Return whether this Interval has the same start and end DateTimes.
     */
    isEmpty(): boolean;

    /**
     * Return whether this Interval's start is after the specified DateTime.
     *
     * @param dateTime
     */
    isAfter(dateTime: DateTime): boolean | IfInvalid<false>;

    /**
     * Return whether this Interval's end is before the specified DateTime.
     *
     * @param dateTime
     */
    isBefore(dateTime: DateTime): boolean | IfInvalid<false>;

    /**
     * Return whether this Interval contains the specified DateTime.
     *
     * @param dateTime
     */
    contains(dateTime: DateTime): boolean | IfInvalid<false>;

    /**
     * "Sets" the start and/or end dates. Returns a newly-constructed Interval.
     *
     * @param values - the values to set
     * @param values.start - the starting DateTime
     * @param values.end - the ending DateTime
     */
    set(values?: IntervalObject): Interval;

    /**
     * Split this Interval at each of the specified DateTimes
     *
     * @param dateTimes - the unit of time to count.
     */
    splitAt(...dateTimes: DateTime[]): Interval[] | IfInvalid<[]>;

    /**
     * Split this Interval into smaller Intervals, each of the specified length.
     * Left over time is grouped into a smaller interval
     *
     * @param duration - The length of each resulting interval.
     */
    splitBy(duration: DurationLike): Interval[] | IfInvalid<[]>;

    /**
     * Split this Interval into the specified number of smaller intervals.
     *
     * @param numberOfParts - The number of Intervals to divide the Interval into.
     */
    divideEqually(numberOfParts: number): Interval[] | IfInvalid<[]>;

    /**
     * Return whether this Interval overlaps with the specified Interval
     */
    overlaps(other: Interval): boolean;

    /**
     * Return whether this Interval's end is adjacent to the specified Interval's start.
     */
    abutsStart(other: Interval): boolean | IfInvalid<false>;

    /**
     * Return whether this Interval's start is adjacent to the specified Interval's end.
     */
    abutsEnd(other: Interval): boolean | IfInvalid<false>;

    /**
     * Return whether this Interval engulfs the start and end of the specified Interval.
     */
    engulfs(other: Interval): boolean | IfInvalid<false>;

    /**
     * Return whether this Interval has the same start and end as the specified Interval.
     */
    equals(other: Interval): boolean | IfInvalid<false>;

    /**
     * Return an Interval representing the intersection of this Interval and the specified Interval.
     * Specifically, the resulting Interval has the maximum start time and the minimum end time of the two Intervals.
     * Returns null if the intersection is empty, meaning the intervals do not intersect.
     */
    intersection(other: Interval): Interval | null;

    /**
     * Return an Interval representing the union of this Interval and the specified Interval.
     * Specifically, the resulting Interval has the minimum start time and the maximum end time of the two Intervals.
     */
    union(other: Interval): Interval;

    /**
     * Merge an array of Intervals into an equivalent minimal set of Intervals.
     * Combines overlapping and adjacent Intervals.
     */
    static merge(intervals: Interval[]): Interval[];

    /**
     * Return an array of Intervals representing the spans of time that only appear in one of the specified Intervals.
     */
    static xor(intervals: Interval[]): Interval[];

    /**
     * Return Intervals representing the spans of time in this Interval that not overlap with any of the specified Intervals.
     */
    difference(...intervals: Interval[]): Interval[];

    /**
     * Returns a string representation of this Interval appropriate for debugging.
     */
    toString(): string | IfInvalid<'Invalid Interval'>;

    /**
     * Returns a localized string representing this Interval. Accepts the same options as the
     * Intl.DateTimeFormat constructor and any presets defined by Luxon, such as
     * {@link DateTime.DATE_FULL} or {@link DateTime.TIME_SIMPLE}. The exact behavior of this method
     * is browser-specific, but in general it will return an appropriate representation of the
     * Interval in the assigned locale. Defaults to the system's locale if no locale has been
     * specified.
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
     * @param formatOpts - Either a DateTime preset or Intl.DateTimeFormat constructor options. Defaults to DateTime.DATE_SHORT
     * @param opts - Options to override the configuration of the start DateTime.
     *
     * @example
     * Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(); //=> 11/7/2022 – 11/8/2022
     * @example
     * Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL); //=> November 7 – 8, 2022
     * @example
     * Interval.fromISO('2022-11-07T09:00Z/2022-11-08T09:00Z').toLocaleString(DateTime.DATE_FULL, { locale: 'fr-FR' }); //=> 7–8 novembre 2022
     * @example
     * Interval.fromISO('2022-11-07T17:00Z/2022-11-07T19:00Z').toLocaleString(DateTime.TIME_SIMPLE); //=> 6:00 – 8:00 PM
     * @example
     * Interval.fromISO("2022-11-07T17:00Z/2022-11-07T19:00Z").toLocaleString({
     *   weekday: "short",
     *   month: "short",
     *   day: "2-digit",
     *   hour: "2-digit",
     *   minute: "2-digit",
     * }); //=> Mon, Nov 07, 6:00 – 8:00 p
     */
    toLocaleString(formatOpts?: Intl.DateTimeFormatOptions, opts?: LocaleOptions): string | IfInvalid<'Invalid Interval'>;

    /**
     * Returns an ISO 8601-compliant string representation of this Interval.
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
     *
     * @param opts - The same options as {@link DateTime#toISO}
     */
    toISO(opts?: ToISOTimeOptions): string | IfInvalid<'Invalid Interval'>;

    /**
     * Returns an ISO 8601-compliant string representation of the dates in this Interval.
     * The time components are ignored.
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
     */
    toISODate(): string | IfInvalid<'Invalid Interval'>;

    /**
     * Returns an ISO 8601-compliant string representation of the times in this Interval.
     * The date components are ignored.
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
     *
     * @param opts - The same options as {@link DateTime.toISO}
     */
    toISOTime(opts?: ToISOTimeOptions): string | IfInvalid<'Invalid Interval'>;

    /**
     * Returns a string representation of this Interval formatted according to the specified format string.
     *
     * @param dateFormat - the format string. This string formats the start and end time. See {@link DateTime.toFormat} for details.
     * @param opts - options
     * @param opts.separator - a separator to place between the start and end representations. Defaults to ' - '.
     */
    toFormat(
        dateFormat: string,
        opts?: {
            separator?: string | undefined;
        },
    ): string | IfInvalid<'Invalid Interval'>;

    /**
     * Return a Duration representing the time spanned by this interval.
     *
     * @param unit - the unit or units (such as 'hours' or 'days') to include in the duration. Defaults to ['milliseconds'].
     * @param opts - options that affect the creation of the Duration
     * @param opts.conversionAccuracy - the conversion system to use. Defaults to 'casual'.
     *
     * @example
     * Interval.fromDateTimes(dt1, dt2).toDuration().toObject() //=> { milliseconds: 88489257 }
     * @example
     * Interval.fromDateTimes(dt1, dt2).toDuration('days').toObject() //=> { days: 1.0241812152777778 }
     * @example
     * Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes']).toObject() //=> { hours: 24, minutes: 34.82095 }
     * @example
     * Interval.fromDateTimes(dt1, dt2).toDuration(['hours', 'minutes', 'seconds']).toObject() //=> { hours: 24, minutes: 34, seconds: 49.257 }
     * @example
     * Interval.fromDateTimes(dt1, dt2).toDuration('seconds').toObject() //=> { seconds: 88489.257 }
     */
    toDuration(unit?: DurationUnit | DurationUnit[], opts?: DiffOptions): Duration;

    /**
     * Run mapFn on the interval start and end, returning a new Interval from the resulting DateTimes
     *
     * @example
     * Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.toUTC())
     * @example
     * Interval.fromDateTimes(dt1, dt2).mapEndpoints(endpoint => endpoint.plus({ hours: 2 }))
     */
    mapEndpoints(mapFn: (d: DateTime) => DateTime): Interval;
}
jasperlin451 commented 1 year ago

So are you saying it's a @types/luxon issue? The actual library does have "private" properties which is what's being stripped out. https://github.com/moment/luxon/blob/75115a4acfc429a1ec50c31a69cabc2202b7c8fa/src/interval.js#L47-L59

samchon commented 1 year ago

If you want to keep using the luxon.Interval with prune() function, you have to send a PR to DefinitelyTyped.

https://github.com/DefinitelyTyped/DefinitelyTyped