Skip to content

Tuple morph with un-annotated parameter infers .infer as never when the return depends on that parameter #1624

Description

@Bas950

🔎 Search Terms

morph never, tuple morph infer never, => morph parameter never, scope morph output never, unannotated morph parameter

🧩 Context

  • ArkType version: 2.2.0
  • TypeScript version: 5.9.3
  • Other context you think may be relevant (JS flavor, OS, etc.): Node 24.11.1, macOS 26.5 (arm64), --strict

🧑‍💻 Repro

Playground Link

import { scope } from "arktype"

const m = scope({
	Base: { text: "string" },

	// ❌ tuple morph, parameter NOT annotated, return DEPENDS on the parameter
	//    -> `.infer` (output) is `never`
	bug: ["Base", "=>", (d) => d],

	// ✅ same morph with the parameter annotated -> `.infer` = { text: string }
	fixedByAnnotation: ["Base", "=>", (d: { text: string }) => d],

	// ✅ morph that IGNORES the parameter -> infers fine
	okIgnoresParam: ["Base", "=>", () => ({ n: 1 })]
}).export()

// Probes — a type error on the line means that case's `.infer` is `never`:
const a: typeof m.bug.infer = { text: "x" }               // ❌ Type '{ text: string }' is not assignable to type 'never'
const b: typeof m.fixedByAnnotation.infer = { text: "x" } // ✅ ok
const c: typeof m.okIgnoresParam.infer = { n: 1 }         // ✅ ok

Summary

In a tuple-expression morph [In, "=>", fn], when fn's parameter is not explicitly annotated and fn's return type depends on that parameter, the resulting type's output (.infer) collapses to never.

Observations:

  • It is not object-specific["string", "=>", (s) => s.length] collapses identically.
  • It is specifically about a parameter-dependent return. A morph that ignores its parameter and returns a fresh value (() => ({ n: 1 })) infers correctly.
  • TypeScript emits no TS7006 (implicit-any) on the un-annotated parameter, so arktype is supplying a contextual input type — yet the output is still never. This suggests a circular inference: the parameter's contextual type appears to depend on the morph's output, which in turn depends on the parameter.
  • Workarounds: annotate the parameter ((d: { text: string }) => d), or use the .pipe() method form (type({ text: "string" }).pipe((d) => d)), both of which infer the output correctly.

This is easy to hit in practice: an object-level normalizing morph (return the same object with one field rewritten) silently turns the whole type — and anything that references it — into never.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    To do

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions