Summary
applyTextInputEvent is the documented way to drive a markup text control from a TypeScript core: hold a TextEditState in the Model, feed it TextInputEvents, store the result. Doing exactly that fails native build with 19 integer-proof errors across 10 lines, all inside sdk/text.ts itself. The app's own code is never named.
native check passes. Only native build fails.
Reproduced against native 0.9.3 (b25cefe), macOS 26.5.2, Zig 0.16.0, from a clean native init.
Repro
// src/core.ts
import { Cmd, asciiBytes } from "@native-sdk/core";
import { type TextEditState, type TextInputEvent, applyTextInputEvent } from "@native-sdk/core/text";
export interface Model {
readonly name: TextEditState; // storing it in the Model is what triggers this
}
export type Msg =
| { readonly kind: "name_edit"; readonly edit: TextInputEvent }
| { readonly kind: "noop" };
export function initialModel(): Model {
return { name: { text: asciiBytes(""), selection: { anchor: 0, focus: 0 }, composition: null } };
}
export function nameText(model: Model): Uint8Array {
return model.name.text;
}
export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
switch (msg.kind) {
case "name_edit": {
const next = applyTextInputEvent(model.name, msg.edit, 120);
if (!next) return model;
return { ...model, name: next };
}
case "noop":
return model;
}
}<!-- src/app.native -->
<column gap="12" padding="16">
<text-field text="{nameText}" placeholder="Type here" on-input="name_edit" />
</column>Actual
sdk/text.ts:79 SC4023 'TextRange.start' / 'TextRange.end' - range failed:
proven range [-Infinity, 9007199254740991]
sdk/text.ts:243,244,325,355,375,394,414,439,459
SC4022 'TextRange.start' / 'TextRange.end' - wholeness failed:
the value may be NaNLine 79 is rangeNormalized, where Math.min is float-classed:
const start = Math.min(r.start, textLen);
const end = Math.min(r.end, textLen);The SC4022 sites are other TextRange constructions - snapTextOffset(...), previousTextCaretOffset(...), { start: result.insertedStart, end: result.insertedEnd }.
Scope
Two things I checked before filing:
- It does not reproduce unless
TextEditStateis a Model field. CallingapplyTextInputEventand storing onlynext.textbuilds fine - the integer slots are only instantiated once the record becomes model state. - Fixing
rangeNormalizedalone is not sufficient. Replacing thatMath.minpair with| 0-pinned ternaries clears all fourSC4023errors at line 79, but the nineSC4022sites remain. This looks systemic acrossTextRangeconstruction rather than a single bad line.
Workaround
| 0 restores integer classification (the remedy in #251), applied both where a derived index is produced and at the slot it lands in. I hand-rolled an equivalent caret/selection editor that compiles under those rules, so the constraint is satisfiable in app code - it just isn't satisfied in the shipped module.
Impact
Without this, a TypeScript core cannot use the SDK's own text engine, so text fields are limited to whatever the app hand-rolls. In my case that meant append/backspace-only editing until I worked out the | 0 pattern - a user could not delete a word from the start of a field.
Activity
2One more data point, reproduced on 0.10.1 (064ca98, macOS 27.0, arm64):
The route of "carry the vocabulary by copying the declarations into the core" is closed too, which pins the workaround to a type-only import:
- Copying
TextRange/TextSelection/TextEditStateverbatim intosrc/core.tswhile still importingapplyTextInputEventfrom@native-sdk/core/textfails with 3×NS1038— "module-scope names are unique across a core's files" — reported atsdk/text.ts:26,:33and:70, each namingsrc/core.tsas the other declaration site. - The compiler's sidecar layer documents the same shape as
SC4010("a tabled type's members gather from more than one declaration site"), so a renamed copy would hit the merged-declaration refusal instead.
Same 19 errors as the original report otherwise; unchanged between 0.9.3 and 0.10.1. The only shape that builds is import type for the state/event types plus an app-owned reducer.
Update: also reproduces on 0.10.1 (
064ca98, Node 24.19.0) — same 19 errors, same lines. Not fixed since 0.9.3, so the version in the original report is stale rather than the bug being.Some findings from attempting a fix, in case they narrow it down:
Math.minis not the cause. Replacing therangeNormalizedpair with plain ternaries changes nothing; the inputs are already float-classed by the time they get there.| 0—snapTextOffset,previousTextOffset,nextTextOffset,snapTextCaretOffset,previousTextCaretOffset,nextTextCaretOffset,previousTextWordOffset,nextTextWordOffset,textLineStartOffset,rangeByteLen— clears all fourSC4023range errors at line 79.SC4022wholeness errors need pins at theTextRangeconstruction sites as well. The proof appears to be required where the value lands, not only where it is produced.| 0sites across the module.That last point is why I have not opened a PR. Twenty-six coercions in a shipped module reads like a workaround rather than a fix, and it suggests the gap is in how the checker infers integrality for values derived from
text.lengthand byte offsets rather than anything specific totext.ts. Happy to send a patch in that shape if it is useful to you, but it seemed more likely you would want to address it a layer down.For reference, the same
| 0discipline — applied both at the producer and at the slot — is what let me hand-roll an equivalent caret/selection editor in app code that compiles cleanly, so the constraint is satisfiable; it just is not satisfied in the shipped module.