Skip to content

refactor: open enums with strict client - #2759

Draft
mariechatfield wants to merge 7 commits into
mainfrom
refactor/marie/SDK-1307-rc4
Draft

refactor: open enums with strict client#2759
mariechatfield wants to merge 7 commits into
mainfrom
refactor/marie/SDK-1307-rc4

Conversation

@mariechatfield

@mariechatfield mariechatfield commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This is a draft specifically so the team can evaluate the open-enum narrowing strategy before it's adopted repo-wide. The relevant commits are split out for that purpose:

  • ccfbf187 -- adds isKnownEnumValue/toKnownEnumValue (src/helpers/openEnum.ts), the mechanism itself, unused until the next commits. This is the one to scrutinize.
  • 2d1cc383 -- two live behavior bugs the mechanism fixes (payment method silently dropped 'Historical', garnishment silently dropped 'child_support'') -- worth keeping regardless of which narrowing mechanism wins.
  • 65e375ed -- mechanical migration of the remaining ~25 call sites onto the helper.
  • d6b7fb37, 1dc6dc33 -- unrelated small fixes for non-exhaustive switches and a discriminated-union unknown-variant guard, needed either way.
  • eecfb3dc -- the rc.4 package bump itself.

Test plan

  • tsc --noEmit clean (0 errors) on this branch against rc.4
  • Full test suite green except one pre-existing timing flake (PayrollConfiguration.test.tsx's gross-up test, passes in isolation -- known flake unrelated to this change)
  • eslint clean on all touched files
  • Team review of the openEnum.ts narrowing approach before this ships

🤖 Generated with Claude Code

mariechatfield and others added 6 commits September 11, 2026 11:50
Republished with strict date parsing re-enabled, so unlike rc.3 this
does not require the RFCDate->Date migration -- only the
OpenEnum/discriminated-union/control-flow fallout from lax mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…itches

Lax mode widens PaymentPeriod and GarnishmentType from closed enums to
OpenEnum (adds Unrecognized<string>), so the exhaustive switch in each
label helper is no longer exhaustive and falls through without a
return. Add a default branch that returns the raw value, matching the
same unrecognized-value fallback already used for RecoveryCasesList.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rd and form

Lax mode's forwardCompatibleUnionsByDefault: tagged-only resolves an
unparseable W4 shape to { w4DataType: "UNKNOWN", raw, isUnknown: true }
instead of one of the two known variants, so code assuming a known
shape and reading a variant-specific field directly now fails to
compile. Guard both sites using each file's existing narrowing idiom
for this union rather than introducing a new one:

- FederalTaxesCard: filingStatus was the one field read without the
  'x' in federalTaxes check already used for the others.
- useFederalTaxesForm: the submit handler's "not loaded" guard now also
  covers "loaded but unparseable" (no version field to submit against).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…wing

The lax-mode API client widens previously-closed enums to
OpenEnum<T> = T | Unrecognized<string> so an unrecognized member
doesn't crash response parsing. Plain `===`/property-existence checks
don't narrow away the Unrecognized branch, so call sites need an
explicit predicate against the enum's own literal members.

Not yet used anywhere -- follow-up commits migrate call sites onto it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ishment narrowing

Two pre-existing predicates narrowed a value against the wrong enum
(a differently-scoped destination/request type) instead of its actual
source enum. Once the source enum gained members the destination
enum didn't share, this silently treated a real, known value as
unrecognized:

- PayrollConfiguration.tsx / PayrollEditEmployee.tsx: paymentMethod
  dropped 'Historical' on submit.
- useDeductionForm.tsx: garnishmentType dropped 'child_support' when
  resolving the fetched deduction's default.

Both now narrow against the full source enum and exclude the specific
value as an explicit, separate step. The Payroll files also add the
deductions[].amountType narrowing the same submit payload needs to
type-check under the OpenEnum client -- left in this commit rather
than split further since it's in the same touched region.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…helper

Mechanical pass closing out the rest of the OpenEnum-narrowing tsc
errors from the lax-mode client migration (SDK-1307) -- 24 files
across Company/Contractor/Employee/Payroll, each swapping a local,
ad-hoc isKnownX check (or an unguarded access that now needs one) for
isKnownEnumValue/toKnownEnumValue. No known-value narrowing bugs in
this batch (unlike the paymentMethod/garnishmentType fixes landed
separately) -- these are all either new guards or straightforward
like-for-like replacements.

TimeOffFlowComponents.tsx is the one exception: it's in the same
bucket (an OpenEnum value needing narrowing) but reuses the existing,
unrelated isEditableTimeOffPolicyType predicate from timeOffPolicyTypes.ts
rather than the new shared helper -- included here since it's the same
class of fix, not because it depends on openEnum.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mariechatfield mariechatfield changed the title SDK-1307: lax-mode client (rc.4) with strict dates -- evaluate OpenEnum narrowing strategy refactor: open enums with strict client Sep 11, 2026
Comment on lines +92 to +93
taxPayerType: toKnownEnumValue(payload.taxPayerType, TaxPayerType, undefined),
filingForm: toKnownEnumValue(payload.filingForm, FilingForm, undefined),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One common category of change: when building the payload to submit a form, don't just send back whatever we got from the API. If we recognize the value as part of the enum, send that; otherwise, send undefined.

This means we never send an unrecognized value back to the server, even if we can handle receiving unrecognized values with a bit more grace

Comment on lines +58 to +61
title: isKnownEnumValue(stepId, Id)
? t(`stepTitles.${stepId}`)
: (step.title ?? stepId),
description: isKnownEnumValue(stepId, Id) ? t(`stepDescriptions.${stepId}`) : '',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Another common category: before assuming that the value from the API maps to a valid translation key, check the enum. If we can't map it to a valid key, display the raw value from the server or an empty string depending on context

Comment on lines +34 to +35
function getSetupStatus(req: TaxRequirementStatesList): ClosedEnum<typeof SetupStatus> {
return toKnownEnumValue(req.setupStatus, SetupStatus, SetupStatus.InProgress)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is the right behavior -- as written, if we get back a setup status we don't recognize, we just use InProgress instead.

This should probably instead have some kind of error for "there is a value, I don't know what it is, so you cannot proceed with setting up your state taxes" treatment

const resolvedDefaults: ContractorPayFormData = useMemo(
() => ({
wageType: contractor?.wageType ?? WageType.Fixed,
wageType: toKnownEnumValue(contractor?.wageType, WageType, WageType.Fixed),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This feels like a reasonable override. When building the default values, try the contractor?.wageType first but if that's an enum value we don't recognize, override it with Fixed

Comment on lines +180 to +181
default:
return value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Honestly we shouldn't have ever had these kind of case statements which fall through and do nothing; it's technically type safe but based on values we don't control end-to-end


const resolvedFetchedGarnishmentType =
isKnownEnumValue(fetchedDeduction?.garnishmentType, GarnishmentType) &&
fetchedDeduction.garnishmentType !== GarnishmentType.ChildSupport

Copy link
Copy Markdown
Contributor Author

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 written to use GARNISHMENT_TYPES const from above instead of excluding ChildSupport manually

Comment on lines 59 to 65
updateGarnishmentRequest: {
...payload,
totalAmount: payload.totalAmount ?? undefined,
active: false,
version: payload.version as string,
garnishmentType: toKnownEnumValue(payload.garnishmentType, GarnishmentType, undefined),
},

@mariechatfield mariechatfield Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Any place we're passing a payload to a change request, that we got directly from a response payload, we need to strip out unknown enum values since in this model we accept getting values we don't know about but we will never send them and the updated typescript from the client enforces this

Comment on lines +69 to +73
onboardingStatus={toKnownEnumValue(
employee.onboardingStatus,
EmployeeOnboardingStatus1,
undefined,
)}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this is another place where instead of falling back on undefined we may want to handle an unknown onboarding status with some kind of standard treatment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant