ErrLookup › Background articles › Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained
Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained
"The 'options' argument must be of type object", "needs to be a string", "expected string, got ..." — invalid argument type errors fire when a library's entry-point validation rejects the type of an argument you passed, before any real work starts. Developers meet this family when calling browser automation APIs (k6 selectOption, setInputFiles), LLM gateways (litellm api_base, dynamic_api_key, usage and timestamp objects), runtime polyfills (Deno's ERR_INVALID_ARG_TYPE family), plugin sandboxes (siyuan registerCapability, port.send), and data clients (chroma ids, turso connections). This article covers the shared mechanism behind 125 documented records across 15 repositories, the most common causes, and the fixes that hold across the whole family: normalize types at the boundary, pass the library's exact accepted shape, and unwrap wrapper types before the call.
Distilled from 125 documented records across 15 repositories.
Background
These errors come from fail-fast validation at a library's public boundary, not from work going wrong mid-operation. Node-style runtimes centralize the pattern: Deno's polyfills throw ERR_INVALID_ARG_TYPE from dedicated validators before a single field is read (spawn options, cipher.update data, timingSafeEqual buffers, Worker env). Other libraries embed the check in a conversion step at a language boundary: k6's Go-side converters (Files.Parse for setInputFiles descriptors, ConvertSelectOptionValues for selectOption values) reject any shape Go cannot map; siyuan's Go kernel applies goja type assertions (IsString, AssertFunction, isJsValueNotNull) to every value crossing out of the plugin sandbox; litellm guards scalar options in get_llm_provider and normalizes logging-payload fields with strict isinstance checks. In every case the throw is a contract statement: the value you handed over is not on the accepted-type list, and the library refused it before touching it.
The narrowness is deliberate. These values are headed somewhere unforgiving: api_base and dynamic_api_key get concatenated into request URLs and Authorization headers in litellm's provider handlers; base paths and directory-entry names get string- or Buffer-concatenated in Deno's readdir join; serializer arrays and selectOption values get iterated element by element; k6 descriptors and tree-sitter's matchLimit cross into Go and wasm. Rejecting early with the offending type echoed back ("got %T", "Received undefined", "of type <class 'int'>", "Was given a function") replaces an obscure downstream crash with a local, actionable one. That is why the messages read as type reports rather than operation failures.
From the caller's seat the message shapes vary — "The \"env\" property must be of type object", "options[value]: expected string, got int", "usage is required, got=... of type ...", "Cannot get raw connection from SQLAlchemy connection" — but the diagnosis is identical: find the accepted-shape list for that argument and match it. Strictness itself is library-specific and sometimes path-specific: chroma auto-wraps a single value into a one-element list through the public collection API but rejects tuples and generators in the direct validator; Deno's Worker env option String()-coerces values inside an object yet rejects a bare string; Deno's timingSafeEqual accepts a broader Buffer/ArrayBuffer/TypedArray/DataView union than Node's Buffers-only API; k6's setInputFiles rejects the plain file paths Playwright accepts and requires base64 descriptor objects. Porting calls verbatim between similar libraries is one of the most reliable ways to meet this family.
The subtlest members involve near-miss types rather than obviously wrong ones. An openai SDK CompletionUsage duck-types identically to litellm's Usage but fails the isinstance check; a plain dict fails the ImageResponse check in litellm's image-cost calculator; a plain JavaScript object fails Immutable.Map.isMap in swagger-ui's paramToIdentifier; a Key instance fails instanceof when two copies of the chromadb package are loaded. Edge behaviors compound the confusion: falsy-but-not-nullish values such as 0 and false sail past a ?? fallback straight into the typeof check, Python ints fail validators that accept only floats, and NaN passes typeof 'number' in tree-sitter's matchLimit check. These guards enforce type identity, not type resemblance — duck typing does not help you here.
Common causes
- Primitive or null where an object is required. The call needs an options, config, or event object and receives null, undefined, a number, boolean, or string. Examples: port.send(null) in siyuan, registerCapability("tool", null, handler), spawn('node'), t.assertSnapshot(value, 5), new Worker(f, { env: 'production' }). The guard fires before any field is read.
- Wrong collection shape. A bare value appears where an array or wrapper object is required: fileSnapshot({ serializers: fn }) instead of [fn], chroma ids as a tuple, set, generator, or numpy array instead of a list, selectOption([1, 2]) instead of [{ index: 1 }], and a Playwright-style path string instead of k6's { name, mimeType, buffer } descriptor.
- Porting calls between similar libraries. Code copied from a sibling API keeps its old argument contract: Playwright setInputFiles('/path') and numeric selectOption indexes fail in k6; Node habits fail Deno polyfills (raw ArrayBuffer to cipher.update, ArrayBuffer instead of a Uint8Array view to copyBytesFrom); an Engine or Session instead of a live Connection fails turso's unwrapping helper.
- Near-miss types from sibling SDKs. Duck-typed equivalents are rejected on class identity: openai.types.CompletionUsage instead of litellm's Usage, a plain dict instead of ImageResponse, a plain object instead of an Immutable.Map in swagger-ui, and a Key instance from a duplicated chromadb package version failing instanceof. Passing .json() or .toJS() output back into library APIs hits the same wall.
- Stringly-typed external data. Values sourced from env vars, config files, URLSearchParams, and JSON arrive as strings where another type is expected: tree-sitter's matchLimit from process.env or a config file throws "Arguments must be numbers"; a config.yaml indentation mistake turns litellm's api_base into a list instead of a scalar string.
- Numeric strictness (int vs float, units, NaN). Validators that accept only floats reject ints and nanosecond values: litellm's timestamp normalizer fails int(time.time()) and time.time_ns() while accepting float(time.time()) or datetime objects. In the other direction, typeof NaN === 'number', so tree-sitter's matchLimit check lets NaN through.
- Wrapped or encoded values not unwrapped. SecretStr, bytes from os.environb, and httpx.URL objects passed to litellm instead of plain strings; hex/base64 token strings passed to timingSafeEqual instead of Buffer.from(x, 'hex'); raw ArrayBuffer or SharedArrayBuffer passed to Deno crypto and buffer APIs instead of a typed-array view.
- Falsy-but-not-nullish and mixed-type values. Values like 0 and false bypass a ?? fallback and reach the typeof check (chroma's Knn key: 0 still throws); a string base path combined with Buffer entry names, or the reverse, fails Deno's internal path join; Go-bound or proxy-wrapped array-likes fail k6's export step with "options: expected array".
What usually fixes it
- Normalize once at the boundary. Coerce external data where it enters your program — list(ids), float(time.time()), Number(matchLimit), Buffer.from(token, 'hex'), str(httpx_url) — and route every call site through one shared helper (a normalize_key, toBuf, or single timestamp utility) instead of patching each call.
- Pass the library's exact accepted shape. Read the accepted-type list in the error and docs and match it literally: the registered string name for swagger-ui's getComponent, a plain string scalar for litellm's api_base, the library's own class instance (litellm Usage, ImageResponse, chroma Key), { index: n } wrappers for k6's selectOption, and arrays where arrays are required.
- Thread library objects end-to-end. Do not re-serialize between receiving and reusing a value: keep the ImageResponse object through cost hooks and call model_dump() only when crossing out of litellm, and keep swagger-ui parameters as Immutable Maps — never .toJS() them back into system APIs.
- Unwrap wrappers and finish encoding before the call. SecretStr.get_secret_value(), decode bytes, base64-encode file payloads into k6 descriptors, view-wrap ArrayBuffers as Uint8Array for Deno's buffer and crypto APIs, and decode both sides of a security comparison to Buffers with matching encodings before timingSafeEqual.
- Add compile-time types and load-time guards. TypeScript option interfaces (QueryOptions, Record<string, string> env, TypedArray parameters) and Python annotations (List[str], sqlalchemy.Connection) turn these errors into build-time failures; runtime guards such as Array.isArray, Number.isFinite, and Immutable.Map.isMap catch external data before the library does.
- Treat strictness as library-specific when porting. Where libraries disagree — chroma auto-wraps single values, Deno's Worker env coerces object values but rejects a bare string, k6 rejects Playwright's file paths — re-check the target library's argument contract instead of copying call sites verbatim.
Documented occurrences
- parsing select options values: %w (grafana/k6)
- Expected IDs to be a list, got {type(ids).__name__} as IDs (chroma-core/chroma)
- api base needs to be a string. api_base={api_base} (BerriAI/litellm)
- usage is required, got={usage} of type {type(usage)} (BerriAI/litellm)
- Expected the second argument to assertSnapshot() to be an options object or a message string (denoland/deno)
- dynamic_api_key needs to be a string. Got type={type(dynamic_api_key).__name__} (BerriAI/litellm)
- Invalid reserved word set name: ${wordset} (tree-sitter/tree-sitter)
- ERR_INVALID_ARG_TYPE: The "options" argument must be of type object. Received ${actual} (denoland/deno)
- invalid event object (siyuan-note/siyuan)
- ERR_INVALID_ARG_TYPE: The "options.serializers" property must be an instance of Array. Received ${actual} (denoland/deno)
- start_time is required, got={start_time} of type {type(start_time)} (BerriAI/litellm)
- parsing setInputFiles parameter: %w (grafana/k6)
- first argument must be a tool name string (siyuan-note/siyuan)
- paramToIdentifier: received a non-Im.Map parameter as input (swagger-api/swagger-ui)
- dynamic_api_key needs to be a string. dynamic_api_key={dynamic_api_key} (BerriAI/litellm)
- parsing setInputFiles parameter: %w (grafana/k6)
- ERR_INVALID_ARG_TYPE: The "view" argument must be of type TypedArray. Received ${view} (denoland/deno)
- options: expected array, got %T (grafana/k6)
- Arguments must be numbers (tree-sitter/tree-sitter)
- ERR_INVALID_ARG_TYPE: The "${name}" argument must be an instance of Buffer, ArrayBuffer, TypedArray, or DataView (denoland/deno)
…and 105 more across the corpus — use search.
Honest provenance: generated on 2026-08-20 from AI-assisted analysis of the linked records. See how records are made.