Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Luxon] Provide generics to allow individual types to be valid/invalid #65078

Merged
merged 4 commits into from
Nov 30, 2023

Conversation

CarsonF
Copy link
Contributor

@CarsonF CarsonF commented Apr 11, 2023

Following up on #64995

In certain situations it's possible to know statically that a DateTime (etc) is valid, even without the throwOnInvalid enabled.

For example, it's probably common to do

DateTime.now().toISO()

This will always return a string, as now() always returns a valid DateTime.
This PR enables TS to know that.

@typescript-bot
Copy link
Contributor

typescript-bot commented Apr 11, 2023

@CarsonF Thank you for submitting this PR!

This is a live comment which I will keep updated.

1 package in this PR

Code Reviews

Because you edited one package and updated the tests (👏), I can help you merge this PR once someone else signs off on it.

You can test the changes of this PR in the Playground.

Status

  • ✅ No merge conflicts
  • ✅ Continuous integration tests have passed
  • ✅ Most recent commit is approved by type definition owners or DT maintainers

All of the items on the list are green. To merge, you need to post a comment including the string "Ready to merge" to bring in your changes.


Diagnostic Information: What the bot saw about this PR
{
  "type": "info",
  "now": "-",
  "pr_number": 65078,
  "author": "CarsonF",
  "headCommitOid": "f6274913355daaecd4c082d222e6d00c0de6ad14",
  "mergeBaseOid": "0dad2b5fe7a3d49263d0b6ce12d2e892df1af9cc",
  "lastPushDate": "2023-04-11T13:55:37.000Z",
  "lastActivityDate": "2023-11-30T20:24:55.000Z",
  "maintainerBlessed": "Waiting for Code Reviews",
  "mergeOfferDate": "2023-11-30T19:56:42.000Z",
  "mergeRequestDate": "2023-11-30T20:24:55.000Z",
  "mergeRequestUser": "CarsonF",
  "hasMergeConflict": false,
  "isFirstContribution": false,
  "tooManyFiles": false,
  "hugeChange": false,
  "popularityLevel": "Critical",
  "pkgInfo": [
    {
      "name": "luxon",
      "kind": "edit",
      "files": [
        {
          "path": "types/luxon/package.json",
          "kind": "package-meta-ok"
        },
        {
          "path": "types/luxon/src/_util.d.ts",
          "kind": "definition"
        },
        {
          "path": "types/luxon/src/datetime.d.ts",
          "kind": "definition"
        },
        {
          "path": "types/luxon/src/duration.d.ts",
          "kind": "definition"
        },
        {
          "path": "types/luxon/src/interval.d.ts",
          "kind": "definition"
        },
        {
          "path": "types/luxon/src/settings.d.ts",
          "kind": "definition"
        },
        {
          "path": "types/luxon/src/zone.d.ts",
          "kind": "definition"
        },
        {
          "path": "types/luxon/test/luxon-tests.module.ts",
          "kind": "test"
        }
      ],
      "owners": [
        "FourwingsY",
        "jsiebern",
        "mastermatt",
        "pietrovismara",
        "dawnmist",
        "ycmjason",
        "Aitor1995",
        "peterblazejewicz",
        "carsonf",
        "hugofpsilva",
        "martin-badin"
      ],
      "addedOwners": [],
      "deletedOwners": [],
      "popularityLevel": "Critical"
    }
  ],
  "reviews": [
    {
      "type": "approved",
      "reviewer": "peterblazejewicz",
      "date": "2023-11-30T19:55:57.000Z",
      "isMaintainer": true
    },
    {
      "type": "stale",
      "reviewer": "koooge",
      "date": "2023-04-22T18:56:19.000Z",
      "abbrOid": "597f3ba"
    }
  ],
  "mainBotCommentID": 1503434831,
  "ciResult": "pass"
}

@typescript-bot typescript-bot moved this from Needs Author Action to Needs Maintainer Review in New Pull Request Status Board Apr 11, 2023
@typescript-bot
Copy link
Contributor

typescript-bot commented Apr 11, 2023

🔔 @FourwingsY @jsiebern @mastermatt @pietrovismara @dawnmist @ycmjason @Aitor1995 @peterblazejewicz @hugofpsilva @martin-badin — please review this PR in the next few days. Be sure to explicitly select Approve or Request Changes in the GitHub UI so I know what's going on.

@jameyg42
Copy link

does Interval have similar characteristics as now()? It almost looks like the only way to create an invalid Interval (which introduces nulls) is with fromISO() which is why i never realized things like Interval.start|end were nullable.

@CarsonF
Copy link
Contributor Author

CarsonF commented Apr 11, 2023

does Interval have similar characteristics as now()? It almost looks like the only way to create an invalid Interval (which introduces nulls) is with fromISO() which is why i never realized things like Interval.start|end were nullable.

All of the Interval factories can produce an invalid interval. Just confirmed with src code now.

@CarsonF
Copy link
Contributor Author

CarsonF commented Apr 11, 2023

Possibly a solution to the isValid check narrowing and the generic being introduced:

const dt = DateTime.fromISO('') as any as DateTime<true> | DateTime<false>;
dt.toISO() // string | null
if (dt.isValid) {
    dt.toISO() // string
}

Since DateTime<true>.isValid: true and DateTime<false>.isValid: false, TS can narrow that type union based on that discriminator.

Idk if it would make sense to change the names to something like:

class DateTimeClass<...> {}

type DateTime = DateTimeClass<true> | DateTimeClass<false>;

That probably wouldn't work.

Maybe something like

type DateTimeValidityUnion = ValidDateTime | InvalidDateTime;

type ValidDateTime = DateTime<true>;
type InvalidDateTime = DateTime<false>;

Then all the factory methods would return 1 of those 3 types.

@RyanCavanaugh RyanCavanaugh moved this from Needs Maintainer Review to Waiting for Code Reviews in New Pull Request Status Board Apr 19, 2023
@typescript-bot
Copy link
Contributor

Re-ping @horiuchi, @WinUP, @koooge, @FourwingsY, @jsiebern, @mastermatt, @pietrovismara, @dawnmist, @ycmjason, @Aitor1995, @peterblazejewicz, @hugofpsilva, @martin-badin:

This PR has been out for over a week, yet I haven't seen any reviews.

Could someone please give it some attention? Thanks!

@typescript-bot typescript-bot added the Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. label Apr 22, 2023
Copy link
Contributor

@koooge koooge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for cron, only the comments have been changed. It is not clear if it is necessary.

@typescript-bot typescript-bot moved this from Waiting for Code Reviews to Needs Maintainer Action in New Pull Request Status Board May 1, 2023
@typescript-bot
Copy link
Contributor

typescript-bot commented May 1, 2023

It has been more than two weeks and this PR still has no reviews.

I'll bump it to the DT maintainer queue. Thank you for your patience, @CarsonF.

(Ping @FourwingsY, @jsiebern, @mastermatt, @pietrovismara, @dawnmist, @ycmjason, @Aitor1995, @peterblazejewicz, @hugofpsilva, @martin-badin.)

@CarsonF CarsonF marked this pull request as draft May 1, 2023 18:44
@typescript-bot typescript-bot moved this from Needs Maintainer Action to Needs Author Action in New Pull Request Status Board May 3, 2023
@danqing
Copy link

danqing commented Nov 1, 2023

It would be great to see some momentum on this PR. @CarsonF is there more work needed here?

@CarsonF
Copy link
Contributor Author

CarsonF commented Nov 16, 2023

@danqing I'm just not confident of the implications of this change. And after the backlash from #64995, I don't want to add another breaking change pain point.

@pvogel1967
Copy link

@CarsonF elegant approach! I'd be willing to give this a spin in our code that suffered badly from #64995 . Do you have sense of where you'd be concerned about the implications of the change?

@pvogel1967
Copy link

Honestly, I love the idea behind the #64995 change as it highlights a lurking issue via type checking, always a good thing. But with that must come a clean way to assert validity per standard type guards, etc. or you wind up peppering your code with ! which anesthetizes people to what is, in general, a very bad practice.

@CarsonF CarsonF force-pushed the luxon/valid-types branch 2 times, most recently from 9fb9e16 to 46a7a39 Compare November 16, 2023 20:01
@CarsonF
Copy link
Contributor Author

CarsonF commented Nov 16, 2023

Just got the branch rebased with all the formatting changes that were conflicting.
I think I have an idea for narrowing with isValid.

@pvogel1967 I think mainly the concern is just around just being able to drop this change in and it be the same. i.e. I haven't added required generics that need to be added everywhere. So type usage doesn't need to change at all in your repos.

@danqing
Copy link

danqing commented Nov 16, 2023

Yea I agree with @pvogel1967 and our codebase has also suffered from #64995 :)

@CarsonF
Copy link
Contributor Author

CarsonF commented Nov 29, 2023

I went with

type Valid = true;
type Invalid = false;

Duration<Valid>
Duration<Invalid>

So we don't have two more type aliases per class.

Copy link
Member

@peterblazejewicz peterblazejewicz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CarsonF thanks!
LGTM!

@typescript-bot typescript-bot added Maintainer Approved Self Merge This PR can now be self-merged by the PR author or an owner and removed Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. labels Nov 30, 2023
@typescript-bot typescript-bot moved this from Needs Maintainer Action to Waiting for Author to Merge in New Pull Request Status Board Nov 30, 2023
@CarsonF
Copy link
Contributor Author

CarsonF commented Nov 30, 2023

Ready to merge :shipit:

@typescript-bot typescript-bot moved this from Waiting for Author to Merge to Recently Merged in New Pull Request Status Board Nov 30, 2023
@typescript-bot typescript-bot merged commit 236b2b8 into DefinitelyTyped:master Nov 30, 2023
3 checks passed
@CarsonF CarsonF deleted the luxon/valid-types branch November 30, 2023 20:25
@typescript-bot typescript-bot removed this from Recently Merged in New Pull Request Status Board Dec 1, 2023
@ab-pm
Copy link
Contributor

ab-pm commented Dec 5, 2023

@CarsonF @pvogel1967

Curious, why don't you guys want to enable throwOnInvalid?

I prefer, when using DateTime to validate data from the user to not follow an exception-handling path, but to just check if it's valid, and if not, to use invalidExplanation and invalidReason in an error message.

We found (after being surprised by the typing issue - which I definitely agree is a positive thing) actually both usage patterns in our code base. In some parts, we used DateTime to validate user input, in other parts we used DateTime to parse API responses (and would have preferred/expected an exception). Having a global flag setting is totally annoying, and the poorest design decision on this topic.

Please share your insights on moment/luxon#1421 to discuss how a (type)safe luxon JS API could look.


/**
* Return an Interval spanning between this DateTime and another DateTime
*
* @param otherDateTime - the other end point of the Interval
*/
until(otherDateTime: DateTime): Interval;
until(otherDateTime: DateTime): IfValid<Interval<Valid>, DateTime<Invalid>, IsValid>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should have been

Suggested change
until(otherDateTime: DateTime): IfValid<Interval<Valid>, DateTime<Invalid>, IsValid>;
until(otherDateTime: DateTime): IfValid<Interval<Valid>, Interval<Invalid>, IsValid>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/moment/luxon/blob/master/src%2Fdatetime.js#L2081

Probably an oversight in JS src, but I felt like I should be true to it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow. Reported as moment/luxon#1551!

@philipmagnusson
Copy link

This is a breaking change in a minor version which causes some issues for us. Do you have any docs on how to migrate from version 3.3.5 to 3.3.6?

@CarsonF
Copy link
Contributor Author

CarsonF commented Jan 9, 2024

@philipmagnusson how is it breaking? I left a couple more comments here, but generally I wouldn't expect any breaking changes here.

@philipmagnusson
Copy link

philipmagnusson commented Jan 10, 2024

You've changed the type of DateTime and the return types of most methods, such as fromISO (see attached images but I guess you know about these changes).

fromISO 3.3.5:
3_3_5_fromISO
DateTime 3.3.6
3_3_6_DateTIme
fromISO 3.3.6
3_3_6_fromISO

To me it's likely to be a breaking change, at least it's risky to do in a patch updates.

Below are some of the outputs that we get from the typescript compile command after we updated from 3.3.5 to 3.3.6:

In order to fix the compilation errors I have to do this in the code for intervals forexample:
Screenshot 2024-01-10 at 11 16 44

"src/components/breakMonitoring/__tests__/BreakMonitor.test.ts(48,3): error TS2322: Type 'Ref<{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn:...' is not assignable to type 
'DeepPartial<ComputedRef<Interval<boolean>>>'.
  Types of property 'value' are incompatible.
    Type '{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn: (d:...' is not assignable to type 'DeepPartial<Interval<boolean>>'.
      Type '{ readonly start: unknown; readonly end: unknown; readonly isValid: false; readonly invalidReason: unknown; readonly invalidExplanation: unknown; length: (unit?: keyof DurationLikeObject) => number; ... 25 more ...; mapEndpoints: (mapFn: (d: DateTime<...>) => DateTime<...>) => Interval<...> | Interval<...>; }' is not assignable to type 'DeepPartial<Interval<boolean>>'.
        Types of property 'invalidReason' are incompatible.
          Type 'unknown' is not assignable to type 'string'.
src/components/breakMonitoring/__tests__/BreakMonitor.test.ts(55,3): error TS2322: Type 'Ref<{ get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; } | { ...; }>' is not assignable to type 'DeepPartial<ComputedRef<DateTime<boolean>>>'.
  Types of property 'value' are incompatible.
    Type '{ get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; } | { ...; }' is not assignable to type 'DeepPartial<DateTime<boolean>>'.
      Type '{ get: (unit: keyof DateTime<boolean>) => number; readonly isValid: false; readonly invalidReason: unknown; readonly invalidExplanation: unknown; readonly locale: unknown; readonly numberingSystem: unknown; ... 66 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => null; }' is not assignable to type 'DeepPartial<DateTime<boolean>>'.
        Types of property 'invalidReason' are incompatible.
          Type 'unknown' is not assignable to type 'string'.
src/composables/__tests__/useBreakMonitoring.test.ts(46,2): error TS2322: Type 'Ref<{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn:...' is not assignable to type 'Ref<Interval<boolean>>'.
  Type '{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn: (d:...' is not assignable to type 'Interval<boolean>'.
    Type '{ readonly start: unknown; readonly end: unknown; readonly isValid: false; readonly invalidReason: unknown; readonly invalidExplanation: unknown; length: (unit?: keyof DurationLikeObject) => number; ... 25 more ...; mapEndpoints: (mapFn: (d: DateTime<...>) => DateTime<...>) => Interval<...> | Interval<...>; }' is not assignable to type 'Interval<boolean>'.
      Types of property 'start' are incompatible.
        Type '{}' is missing the following properties from type 'DateTime<true>': get, isValid, invalidReason, invalidExplanation, and 66 more.
src/composables/__tests__/useBreakMonitoring.test.ts(94,13): error TS2345: Argument of type 'Ref<{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn:...' is not assignable to parameter of type 'Ref<Interval<boolean>>'.
src/composables/__tests__/useBreakMonitoring.test.ts(99,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(100,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(113,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(114,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(135,13): error TS2345: Argument of type 'Ref<{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn:...' is not assignable to parameter of type 'Ref<Interval<boolean>>'.
src/composables/__tests__/useBreakMonitoring.test.ts(144,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(145,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(160,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(161,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(182,13): error TS2345: Argument of type 'Ref<{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn:...' is not assignable to parameter of type 'Ref<Interval<boolean>>'.
src/composables/__tests__/useBreakMonitoring.test.ts(187,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(188,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(219,13): error TS2345: Argument of type 'Ref<{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn:...' is not assignable to parameter of type 'Ref<Interval<boolean>>'.
src/composables/__tests__/useBreakMonitoring.test.ts(224,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(225,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(234,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(235,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(249,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(250,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(278,13): error TS2345: Argument of type 'Ref<{ readonly start: { get: (unit: keyof DateTime<boolean>) => number; readonly isValid: true; readonly invalidReason: string; readonly invalidExplanation: string; readonly locale: string; ... 67 more ...; toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string; }; ... 30 more ...; mapEndpoints: (mapFn:...' is not assignable to parameter of type 'Ref<Interval<boolean>>'.
src/composables/__tests__/useBreakMonitoring.test.ts(283,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(284,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(293,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(294,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(311,36): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/composables/__tests__/useBreakMonitoring.test.ts(312,32): error TS2339: Property 'toISO' does not exist on type 'unknown'.
src/pages/provider/campaign/orderline/__tests__/Orderline.test.ts(246,3): error TS2322: Type 'DateTime<true> | DateTime<false>' is not assignable to type 'DateTime<true> | Promise<DateTime<true>>'.
  Type 'DateTime<false>' is not assignable to type 'DateTime<true> | Promise<DateTime<true>>'.
    Type 'DateTime<false>' is not assignable to type 'DateTime<true>'.
      Type 'false' is not assignable to type 'true'.
src/utils/__tests__/performanceUtils.test.ts(508,5): error TS2322: Type 'DateTime<true> | DateTime<false>' is not assignable to type 'DateTime<true> | Promise<DateTime<true>>'.
src/utils/__tests__/performanceUtils.test.ts(519,5): error TS2322: Type 'DateTime<true> | DateTime<false>' is not assignable to type 'DateTime<true> | Promise<DateTime<true>>'."

Here is another example that was introduced in 3.4.0:
Screenshot 2024-01-10 at 14 09 02

And this is how I worked around the issue:

Screenshot 2024-01-10 at 14 07 40

@CarsonF
Copy link
Contributor Author

CarsonF commented Jan 10, 2024

@philipmagnusson we don't have a lot of control on versioning with breaking changes. We follow the src packages versioning, so really we can only do patch releases ourselves.

It's a bit hard to know what the problem you are having is without being able to get my hands on the code.
I would expect

const breakInterval: Ref<Interval> = ref(Interval.fromISO())

to work fine.
Unless ref() is doing something with DeepPartial. I would guess that that's one incompatibility point. DeepPartial doesn't know which types it shouldn't start modifying. If it were smarter it would know to treat Interval as scalar-like, and leave it untouched, but it's not. It's a bit like hacky reflection in that way.

I still recommend enabling luxon to throw on invalid input. Which does away with all of this validity generics.

import {Settings} from 'luxon';

Settings.throwOnInvalid = true;

declare module 'luxon' {
  interface TSSettings {
    throwOnInvalid: true;
  }
}

@philipmagnusson
Copy link

Unfortunately, const breakInterval: Ref<Interval> = ref(Interval.fromISO()) does not work. I've had to do these type casts through our code base, for example here:
Screenshot 2024-01-13 at 13 19 58

where windowInterval is a ComputedRef https://vuejs.org/api/reactivity-core.html#computed

Screenshot 2024-01-13 at 13 20 38

I'm not sure what it would been for our code base to opt-in to throwOnInvalid so I'd rather not do it, but I'll look it up.

@CarsonF
Copy link
Contributor Author

CarsonF commented Jan 13, 2024

@philipmagnusson I'm assuming you are assuming all temporal inputs are valid, and not checking the valid prop, or invalidReason. The opt in just converts those "lazy invalid objects" into thrown errors. I've never actually had an error produced, since these inputs are rarely hand crafted and more rare they are invalid. So I'd strongly recommend that opt-in. It's a poor default decision from Luxon.

Vue's ref and computed seem simple and shouldn't have any conflicts.
If you could get me a reproduction repo that would be helpful.

@philipmagnusson
Copy link

philipmagnusson commented Jan 15, 2024

hmm... I don't understand. Should the settings you posted above remove the DateTime<true|false> definitions? I tried it but I don't think it works, or I don't understand how to insert the settings you posted above:

import {Settings} from 'luxon';

Settings.throwOnInvalid = true;

declare module 'luxon' {
  interface TSSettings {
    throwOnInvalid: true;
  }
}

Tried to put it in ./luxon.ts file and include it in tsconfig.json and import it in the index.ts of our app as suggested here: #64995 (comment) and also tried to do this:

create a luxon.d.ts file:

export {}
declare module 'luxon' {
  interface TSSettings {
    throwOnInvalid: true
  }
}

and do the setting:

import { Settings } from 'luxon';

Settings.throwOnInvalid = true;

in the index.ts file of our app.

The DateTime is still defined as:

class DateTime<IsValid extends boolean = true>

and e.g. setLocale returns DateTime<true> which causes the issues for us.

@trevorr
Copy link
Contributor

trevorr commented Mar 14, 2024

+1 that this is breaking change:

src/util/recurrence.test.ts:15:64 - error TS2345: Argument of type 'DateTime<true> | DateTime<false>' is not assignable to parameter of type 'DateTime<true> | undefined'.
  Type 'DateTime<false>' is not assignable to type 'DateTime<true>'.
    Type 'false' is not assignable to type 'true'.

@CarsonF
Copy link
Contributor Author

CarsonF commented Mar 14, 2024

@philipmagnusson sorry I missed your reply. The signature you posted

class DateTime<IsValid extends boolean = true>

Means that the throwOnInvalid is being applied correctly. The default validity is always true instead of true or false (aka boolean).

How is "setLocale returning DateTime<true>" an issue for you?

Would you open a new discussion or issue to continue this?


@trevorr "+1" comments are a frowned upon practice. It causes notification spam to maintainers and everyone participating in the thread without adding any value. Please don't do that anywhere. This is exactly what reactions are for.

Both comments & reactions are easily ignored on merged PRs.
So, please also open a new discussion or issue to continue this.

That error doesn't tell me anything without seeing the source code that produced it.
Also are you using throwOnInvalid or not?

@trevorr
Copy link
Contributor

trevorr commented Mar 14, 2024

without adding any value. Please don't do that anywhere

@CarsonF That's a bit condescending. It wasn't just a +1, since I included an additional error mode not yet mentioned. The error itself makes clear that the new type argument and its new usage within the interface are inherently breaking.

Here's the test that now fails to type check:

const startDate = DateTime.fromISO("2020-02-29T08:00:00Z", { zone: "utc" });
const pt2h = Duration.fromObject({ hours: 2 });

describe("formatRecurrenceRule", () => {
  it("formats none", () => {
    // error TS2345: Argument of type 'DateTime<true> | DateTime<false>' is not assignable to parameter of type 'DateTime<true> | undefined'.
    expect(formatRecurrenceRule({ type: RecurrenceType.None }, startDate, pt2h)).toBeNull();
  });

Here's the signature of that function:

export function formatRecurrenceRule(
  recur: Recurrence,
  startDate = DateTime.utc(),
  duration?: Duration
): string | null {

@CarsonF
Copy link
Contributor Author

CarsonF commented Mar 14, 2024

The error itself makes clear that the new type argument and its new usage within the interface are inherently breaking.

I understand typescript's error message. I guess what I meant was I still needed more info about the usage.
Your new signature definitely helps.

So here's the rub.

DateTime.utc()          // always valid
DateTime.fromISO('...') // maybe valid depending on string

Now your startDate argument is inferring the type from this default value.
So TS infers that the type is always a valid DateTime.
You can give an explicit type to tell TS that this is actually a looser type.

export function formatRecurrenceRule(
  recur: Recurrence,
  startDate: DateTime = DateTime.utc(),
  duration?: Duration
): string | null {

This is the same principle as

// Loosen name to any string, not just "Carson"
const name: string = "Carson";

This is an unfortunate consequence of this PR, but I don't see any way around it.
We are constrained by the decision of the JS library.

@trevorr
Copy link
Contributor

trevorr commented Mar 14, 2024

Yeah, that totally makes sense and thanks for the explanation. That change does fix all the compile errors in my tests. Perhaps this is mainly an issue when using type inference.

Maybe what's needed is some sort of migration guide? It took a bit of digging to find this PR and figure out what was going on. Has anyone asked if the Luxon maintainers would accept some Typescript documentation and possibly even the typings into their repo? It seems like you (@CarsonF) did that in this PR for how to configure the default behavior. Sadly, you only got some not helpful feedback from a non-maintainer.

@CarsonF
Copy link
Contributor Author

CarsonF commented Mar 14, 2024

I don't know what Issac's views on TS are. I've never seen him anywhere near these type PRs/issues (here or in his own repo). So yeah at the end of the day this is not a TS library, and TS is a second class citizen for it. So that excludes docs, breaking changes, etc related to types.
Unfortunate for sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author is Owner The author of this PR is a listed owner of the package. Critical package Maintainer Approved Self Merge This PR can now be self-merged by the PR author or an owner
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet