Skip to content

SDK type safety: mutation option bags and projected list items are Record<string, unknown> — field typos/wrong types compile clean under strict #601

Closed

Description

@unbraind

Summary

The SDK's primary programmatic surface leans on Record<string, unknown> for both mutation inputs and query outputs, so TypeScript provides almost no protection for the exact operations agents run most. Field-name typos and wrong value types compile clean under strict and fail only at runtime, and reading a listed item's id is not type-safe.

Environment

  • pm-cli 2026.7.18, @unbrained/pm-cli/sdk, tsc strict, moduleResolution: bundler.

Inputs — mutation option bags are untyped

dist/sdk/runtime.d.ts:

export type PmActionOptions = Record<string, unknown>;                       // :130
export type PmActionInput   = PmActionOptions & { … };                       // :169
export type PmClientMutationOptions = PmActionOptions & { fullChangedFields?…, idOnly?… };  // :188
export type PmClientFullMutationOptions = Omit<PmClientMutationOptions, "fullChangedFields" | "idOnly">;  // :195
create(options?: PmClientFullMutationOptions): Promise<CreateResult>;        // PmClient.create
export declare function create(options?: PmClientFullMutationOptions, …): Promise<CreateResult>;

Because the base is Record<string, unknown>, the intersection accepts any key of any type:

import { create } from "@unbrained/pm-cli/sdk";
await create({ titel: "typo", priority: "high", nonsense: 42 });  // ✅ compiles under strict — all wrong

titel (typo for title), a string priority (should be number), and a garbage key all type-check. The valid fields are already known to the package — CREATE_FLAG_CONTRACTS, UPDATE_FLAG_CONTRACTS, TOOL_CREATE_OPTION_CONTRACTS, etc. are exported — but none of that shape reaches the option type.

Outputs — projected list items are untyped

// dist/sdk/query/list.d.ts
export type ListProjectedItem = Record<string, unknown>;                     // :95
export type ListResultItem = ListedItem | ListTreeItem | ListProjectedItem;  // :97 — union has a bare-record arm

So (await client.list()).items[i].id is unknown (the ListProjectedItem arm poisons the union) and requires manual narrowing/casting for the single most common field access.

Impact

For an agent-integration author, the headline value of a typed SDK is "the compiler catches my field typos and wrong enums before I ship." Here it catches neither on input nor output — the two hottest paths. This is especially costly for LLM agents, which routinely emit near-miss field names (titel, desc, assignee vs assigned_to) that a real typed surface would reject at author time.

Suggested fix

  1. Give create/update/claimNext/… typed option interfaces (title: string, priority?: number, status?: ItemStatus, tags?: string[], …) derived from the existing *_FLAG_CONTRACTS/*_OPTION_CONTRACTS constants, instead of Record<string, unknown>. Keep an escape hatch ([custom: string]: unknown) only for user-defined schema fields, so core fields still get excess-property checking.
  2. Type ListedItem.id/ListProjectedItem so .id (and other guaranteed fields) is string, not unknown; drop the bare-Record arm from ListResultItem or gate it behind an explicit projection generic.

Confidence: high (types quoted verbatim from the shipped .d.ts; typo example type-checks under strict).

Activity

  1. coderabbitai commented on Jul 18, 2026

    @coderabbitai
    🔗 Related PRs

    #49 - feat(schema): pm schema add-type for config-driven custom item types [closed]
    #466 - Expose SDK read primitives and auto PM context hint [merged]
    #477 - feat(sdk): expand pm primitives and lifecycle aliases [merged]
    #481 - Expose SDK context primitives and closed_at metadata [merged]
    #499 - Harden SDK schema-driven input fidelity and package preflight [merged]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. unbraind commented on Jul 19, 2026

    @unbraind
    OwnerAuthor

    pm tracker item: pm-x29o

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions