From 914754e99fd07d9b39dace5c58d438fd37970697 Mon Sep 17 00:00:00 2001 From: Aswin S Vijay Date: Fri, 1 May 2026 20:03:00 +0000 Subject: [PATCH 1/2] fix label names when `internalSignature` has optional params --- ark/type/fn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ark/type/fn.ts b/ark/type/fn.ts index bd39724ee..560909365 100644 --- a/ark/type/fn.ts +++ b/ark/type/fn.ts @@ -38,7 +38,7 @@ export type BaseFnParser<$ = {}> = < externalSignature extends Fn = ( ...args: applyElementLabels< distill.In, - Parameters + Required> > ) => args extends readonly [...unknown[], ":", unknown] ? distill.Out : ReturnType From d84499d91debf1b749d557c49e24b5a8804bd88e Mon Sep 17 00:00:00 2001 From: David Blass Date: Thu, 17 Sep 2026 15:24:55 -0400 Subject: [PATCH 2/2] test: cover labels for an implementation that annotates its optional parameter The fix had no test because tuple labels look untestable - they are cosmetic and carry no runtime representation. They are still part of the inferred type, so `attest(...).type.toString` pins them exactly. The case is narrower than the issue suggests. A contextually inferred parameter already labels correctly, both alone and alongside others: type.fn("number?")(n => n) // (n?: number | undefined) type.fn("string", "number?")((a, b) => {}) // (a: string, b?: number | undefined) It is annotating the optional parameter that breaks it, since only then does `Parameters` carry an optional element - which `applyElementLabels` cannot match against `labels extends [unknown, ...infer labelsTail]`, so it drops every label rather than just that one: type.fn("number?")((n?: number) => n) // (args_0?: number | undefined) The test uses that last form, with the parameter optional on both sides. The issue's own repro instead pairs a required `"number"` with an optional `b?`, which reproduces the bug but asks for something incoherent: the schema requires the argument, so an implementation marking it optional describes a call that cannot happen. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AvpxQ2tfRbpXYxMH3NbXou --- ark/repo/scratch.ts | 15 +++++++++++++++ ark/type/__tests__/fn.test.ts | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index 3b2a888ba..64c19f6a2 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -10,3 +10,18 @@ const darkColorLiteral = type.enumerated( ) const darkColorRegex = type(`/^(${join(options, "|")})Dark$/`) + +const literalValues = (t: type.Any): t[] => { + const literals: unknown[] = [] + for (const branch of t.internal.branches) { + if (branch.hasKind("unit")) { + literals.push(branch.unit) + } + } + return literals as never +} + +const palette = type.enumerated("red", "blue") + +// ["blue", "red"] +literalValues(palette) diff --git a/ark/type/__tests__/fn.test.ts b/ark/type/__tests__/fn.test.ts index 7b0348644..312225ff6 100644 --- a/ark/type/__tests__/fn.test.ts +++ b/ark/type/__tests__/fn.test.ts @@ -402,6 +402,22 @@ contextualize(() => { }) }) + // https://github.com/arktypeio/arktype/issues/1614 + it("labels an implementation that annotates its optional parameter", () => { + // labels come from the implementation's parameter names, which are only + // cosmetic. contextually inferred parameters already labelled correctly; + // it is annotating the optional one that stopped applyElementLabels from + // matching, dropping every label rather than just that one: + // (args_0?: number | undefined) => number | undefined + const f = type.fn("number?")((n?: number) => n) + + attest(f).type.toString.snap(`TypedFn< + (n?: number | undefined) => number | undefined, + {}, + {} +>`) + }) + describe("scoped", () => { it("scoped param and return", () => { const $ = type.scope({