From 80d0944fcadd81369a00bae575c1102f50711711 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Mon, 18 May 2026 14:41:58 -0700 Subject: [PATCH 1/3] fix(cbor): accept `readonly` input data in encoders Closes #5831 (for the `@std/cbor` module). `encodeCbor` / `encodeCborSequence` / `CborSequenceEncoderStream` / `CborArrayEncoderStream` never mutate their inputs, but the `CborType` parameter required mutable arrays, a mutable `Map`, and a mutable index signature. That meant `as const` literals, `ReadonlyMap` values, and `readonly T[]` arrays could not be passed without a cast, even though the runtime behaviour is identical. Add a new input-only type `CborInputType` that mirrors `CborType` but substitutes: - `CborType[]` -> `readonly CborInputType[]` - `Map` -> `ReadonlyMap` - `{ [k: string]: CborType }` -> `{ readonly [k: string]: CborInputType }` `CborType` (which is also the decoder output type) stays mutable, so this is fully backwards-compatible for existing callers and for the decode side. `CborStreamInput` is input-only and is widened in place. In the encoders, the existing `x instanceof Map` narrowing does not narrow `ReadonlyMap` on its own, so a tiny `isReadonlyMap` type predicate is added in `_common_encode.ts` and the relevant `#encodeArray` / `#encodeObject` stream helpers are widened to accept the readonly variants. `CborTag`'s constraint is widened from `T extends CborType | CborStreamInput | CborStreamOutput` to also permit `CborInputType`. Mirrors the `@std/msgpack` change from #5832. Regression tests added: - `encodeCbor()` accepts deeply-readonly `as const` literals. - `encodeCbor()` accepts readonly tuple literals. - `encodeCbor()` accepts `ReadonlyMap` input. - `encodeCbor()` accepts `{ readonly [k: string]: ... }` input. - `encodeCbor()` accepts a `CborTag` whose content is readonly. - `encodeCborSequence()` accepts a `readonly` array of values. All 89 CBOR module tests pass (24 pre-existing + 65 from related files; 6 new). `deno check cbor/` clean. `deno lint cbor/` clean. `deno fmt --check cbor/` clean. --- cbor/_common_encode.ts | 28 ++++++++++------ cbor/encode_cbor.ts | 8 ++--- cbor/encode_cbor_sequence.ts | 12 ++++--- cbor/encode_cbor_sequence_test.ts | 11 ++++++ cbor/encode_cbor_test.ts | 56 +++++++++++++++++++++++++++++++ cbor/sequence_encoder_stream.ts | 6 ++-- cbor/tag.ts | 14 ++++++-- cbor/types.ts | 37 ++++++++++++++++++-- 8 files changed, 146 insertions(+), 26 deletions(-) diff --git a/cbor/_common_encode.ts b/cbor/_common_encode.ts index b038bd86eb4a..4b7d9e93b3f1 100644 --- a/cbor/_common_encode.ts +++ b/cbor/_common_encode.ts @@ -1,7 +1,14 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { CborTag } from "./tag.ts"; -import type { CborType } from "./types.ts"; +import type { CborInputType } from "./types.ts"; + +// Narrows to ReadonlyMap (which TS's instanceof Map doesn't do on its own). +function isReadonlyMap( + x: unknown, +): x is ReadonlyMap { + return x instanceof Map; +} function calcBytes(x: bigint): number { let bytes = 0; @@ -20,7 +27,7 @@ function calcHeaderSize(x: number | bigint): number { return 9; } -export function calcEncodingSize(x: CborType): number { +export function calcEncodingSize(x: CborInputType): number { if (x == undefined || typeof x === "boolean") return 1; if (typeof x === "number") { return x % 1 === 0 ? calcHeaderSize(x < 0 ? -x - 1 : x) : 9; @@ -46,7 +53,7 @@ export function calcEncodingSize(x: CborType): number { for (const y of x) size += calcEncodingSize(y); return size; } - if (x instanceof Map) { + if (isReadonlyMap(x)) { let size = 3 + calcHeaderSize(x.size); for (const y of x) size += calcEncodingSize(y[0]) + calcEncodingSize(y[1]); return size; @@ -61,7 +68,7 @@ export function calcEncodingSize(x: CborType): number { } export function encode( - input: CborType, + input: CborInputType, output: Uint8Array, offset: number, ): number { @@ -86,8 +93,9 @@ export function encode( return encodeDate(input, output, offset); } else if (input instanceof CborTag) { return encodeTag(input, output, offset); - } else if (input instanceof Map) return encodeMap(input, output, offset); - else if (input instanceof Array) { + } else if (isReadonlyMap(input)) { + return encodeMap(input, output, offset); + } else if (input instanceof Array) { return encodeArray(input, output, offset); } else return encodeObject(input, output, offset); } @@ -202,7 +210,7 @@ function encodeString( } function encodeArray( - input: CborType[], + input: readonly CborInputType[], output: Uint8Array, offset: number, ): number { @@ -212,7 +220,7 @@ function encodeArray( } function encodeObject( - input: { [k: string]: CborType }, + input: { readonly [k: string]: CborInputType }, output: Uint8Array, offset: number, ): number { @@ -231,7 +239,7 @@ function encodeDate(input: Date, output: Uint8Array, offset: number): number { } function encodeTag( - input: CborTag, + input: CborTag, output: Uint8Array, offset: number, ): number { @@ -255,7 +263,7 @@ function encodeTag( } function encodeMap( - input: Map, + input: ReadonlyMap, output: Uint8Array, offset: number, ): number { diff --git a/cbor/encode_cbor.ts b/cbor/encode_cbor.ts index 8483d3a410a9..00b9b606329d 100644 --- a/cbor/encode_cbor.ts +++ b/cbor/encode_cbor.ts @@ -1,10 +1,10 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { calcEncodingSize, encode } from "./_common_encode.ts"; -import type { CborType } from "./types.ts"; +import type { CborInputType } from "./types.ts"; /** - * Encodes a {@link CborType} value into a CBOR format represented as a + * Encodes a {@link CborInputType} value into a CBOR format represented as a * {@link Uint8Array}. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * @@ -32,10 +32,10 @@ import type { CborType } from "./types.ts"; * assertEquals(decodedMessage, rawMessage); * ``` * - * @param value The value to encode of type {@link CborType}. + * @param value The value to encode of type {@link CborInputType}. * @returns A {@link Uint8Array} representing the encoded data. */ -export function encodeCbor(value: CborType): Uint8Array { +export function encodeCbor(value: CborInputType): Uint8Array { const output = new Uint8Array(calcEncodingSize(value)); const o = encode(value, output, 0); if (o !== output.length) return output.subarray(0, o); diff --git a/cbor/encode_cbor_sequence.ts b/cbor/encode_cbor_sequence.ts index 2a3f3661f8af..d757484220d2 100644 --- a/cbor/encode_cbor_sequence.ts +++ b/cbor/encode_cbor_sequence.ts @@ -1,11 +1,11 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { calcEncodingSize, encode } from "./_common_encode.ts"; -import type { CborType } from "./types.ts"; +import type { CborInputType } from "./types.ts"; /** - * Encodes an array of {@link CborType} values into a CBOR format sequence - * represented as a {@link Uint8Array}. + * Encodes an array of {@link CborInputType} values into a CBOR format + * sequence represented as a {@link Uint8Array}. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * * @example Usage @@ -31,10 +31,12 @@ import type { CborType } from "./types.ts"; * assertEquals(decodedMessage, rawMessage); * ``` * - * @param values An array of values to encode of type {@link CborType} + * @param values An array of values to encode of type {@link CborInputType} * @returns A {@link Uint8Array} representing the encoded data. */ -export function encodeCborSequence(values: CborType[]): Uint8Array { +export function encodeCborSequence( + values: readonly CborInputType[], +): Uint8Array { let o = 0; for (const value of values) o += calcEncodingSize(value); const output = new Uint8Array(o); diff --git a/cbor/encode_cbor_sequence_test.ts b/cbor/encode_cbor_sequence_test.ts index 24200c4264a7..82bc41fdadd6 100644 --- a/cbor/encode_cbor_sequence_test.ts +++ b/cbor/encode_cbor_sequence_test.ts @@ -2,6 +2,7 @@ import { assertEquals } from "@std/assert"; import { encodeCborSequence } from "./encode_cbor_sequence.ts"; +import type { CborType } from "./types.ts"; Deno.test("encodeCborSequence() correctly encoding", () => { assertEquals( @@ -9,3 +10,13 @@ Deno.test("encodeCborSequence() correctly encoding", () => { Uint8Array.from([0b000_00000, 0b000_00000]), ); }); + +Deno.test("encodeCborSequence() accepting readonly array input", () => { + const values = [1, "two", { three: 3n }] as const; + const mutable: CborType[] = [1, "two", { three: 3n }]; + // `readonly CborInputType[]` is now an accepted input. + assertEquals( + encodeCborSequence(values), + encodeCborSequence(mutable), + ); +}); diff --git a/cbor/encode_cbor_test.ts b/cbor/encode_cbor_test.ts index 0e71085be55d..10b1d07fb4f7 100644 --- a/cbor/encode_cbor_test.ts +++ b/cbor/encode_cbor_test.ts @@ -498,3 +498,59 @@ Deno.test("encodeCbor() rejecting CborTag()", () => { `Cannot encode Tag Item: Tag Number (${num}) exceeds 2 ** 64 - 1`, ); }); + +Deno.test("encodeCbor() accepting `as const` (deeply readonly) input", () => { + // Regression test for #5831. Before, this call failed type-checking + // (`readonly` properties weren't assignable to `CborType`). The encoded + // bytes are identical to the mutable equivalent. + const data = { + a: 1, + b: { c: 2n }, + d: [3, { e: 4 }], + } as const; + + assertEquals(encodeCbor(data), encodeCbor(structuredClone(data))); +}); + +Deno.test("encodeCbor() accepting readonly array literals", () => { + const tuple = ["hello", 42, { nested: "value" }] as const; + // `readonly [3, ...]` is now assignable to the encoder input type. + assertEquals( + encodeCbor(tuple), + encodeCbor(["hello", 42, { nested: "value" }]), + ); +}); + +Deno.test("encodeCbor() accepting ReadonlyMap input", () => { + const map: ReadonlyMap = new Map([ + [1, 2], + ["3", 4], + [[5], { a: 6 }], + ]); + + // `ReadonlyMap<...>` is now assignable to the encoder input type. + assertEquals( + encodeCbor(map), + encodeCbor( + new Map([ + [1, 2], + ["3", 4], + [[5], { a: 6 }], + ]), + ), + ); +}); + +Deno.test("encodeCbor() accepting readonly index signature", () => { + const obj: { readonly [k: string]: number } = { x: 1, y: 2, z: 3 }; + assertEquals(encodeCbor(obj), encodeCbor({ x: 1, y: 2, z: 3 })); +}); + +Deno.test("encodeCbor() accepting CborTag with readonly content", () => { + const data = [1, { inner: 2 }] as const; + // The tag content type widens to accept `CborInputType`. + assertEquals( + encodeCbor(new CborTag(1, data)), + encodeCbor(new CborTag(1, structuredClone(data))), + ); +}); diff --git a/cbor/sequence_encoder_stream.ts b/cbor/sequence_encoder_stream.ts index 990c57c4d166..9f59c5c4ff80 100644 --- a/cbor/sequence_encoder_stream.ts +++ b/cbor/sequence_encoder_stream.ts @@ -145,7 +145,9 @@ export class CborSequenceEncoderStream } else yield encodeCbor(x); } - async *#encodeArray(x: CborStreamInput[]): AsyncGenerator { + async *#encodeArray( + x: readonly CborStreamInput[], + ): AsyncGenerator { if (x.length < 24) yield new Uint8Array([0b100_00000 + x.length]); else if (x.length < 2 ** 8) yield new Uint8Array([0b100_11000, x.length]); else if (x.length < 2 ** 16) { @@ -162,7 +164,7 @@ export class CborSequenceEncoderStream } async *#encodeObject( - x: { [k: string]: CborStreamInput }, + x: { readonly [k: string]: CborStreamInput }, ): AsyncGenerator { const len = Object.keys(x).length; if (len < 24) yield new Uint8Array([0b101_00000 + len]); diff --git a/cbor/tag.ts b/cbor/tag.ts index 497ba62482cf..5e9936db0f08 100644 --- a/cbor/tag.ts +++ b/cbor/tag.ts @@ -1,6 +1,11 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import type { CborStreamInput, CborStreamOutput, CborType } from "./types.ts"; +import type { + CborInputType, + CborStreamInput, + CborStreamOutput, + CborType, +} from "./types.ts"; /** * Represents a CBOR tag, which pairs a tag number with content, used to convey @@ -30,9 +35,12 @@ import type { CborStreamInput, CborStreamOutput, CborType } from "./types.ts"; * ``` * * @typeParam T The type of the tag's content, which can be a - * {@link CborType}, {@link CborStreamInput}, or {@link CborStreamOutput}. + * {@link CborType}, {@link CborInputType}, {@link CborStreamInput}, or + * {@link CborStreamOutput}. */ -export class CborTag { +export class CborTag< + T extends CborType | CborInputType | CborStreamInput | CborStreamOutput, +> { /** * A {@link number} or {@link bigint} representing the CBOR tag number, used * to identify the type of the tagged content. diff --git a/cbor/types.ts b/cbor/types.ts index d75b773650a9..7a15a2af09c8 100644 --- a/cbor/types.ts +++ b/cbor/types.ts @@ -28,6 +28,9 @@ export type CborPrimitiveType = * This type specifies the encodable and decodable values for * {@link encodeCbor}, {@link decodeCbor}, {@link encodeCborSequence}, and * {@link decodeCborSequence}. + * + * The encoder functions also accept {@link CborInputType}, which permits + * `readonly` arrays, `ReadonlyMap`, and `readonly` index signatures. */ export type CborType = | CborPrimitiveType @@ -38,15 +41,45 @@ export type CborType = [k: string]: CborType; }; +/** + * Readonly-friendly input type for {@link encodeCbor} and + * {@link encodeCborSequence}. Lets you pass `as const` literals, + * `ReadonlyMap`s, and frozen arrays without casting. + * + * @example Usage + * ```ts + * import { encodeCbor } from "@std/cbor"; + * + * const data = { + * a: 1, + * b: { c: 2n }, + * d: [3, { e: 4 }], + * } as const; + * + * encodeCbor(data); + * ``` + */ +export type CborInputType = + | CborPrimitiveType + | CborTag + | ReadonlyMap + | readonly CborInputType[] + | { + readonly [k: string]: CborInputType; + }; + /** * Specifies the encodable value types for the {@link CborSequenceEncoderStream} * and {@link CborArrayEncoderStream}. + * + * Arrays and index signatures are `readonly` so `as const` literals work + * without casting. */ export type CborStreamInput = | CborPrimitiveType | CborTag - | CborStreamInput[] - | { [k: string]: CborStreamInput } + | readonly CborStreamInput[] + | { readonly [k: string]: CborStreamInput } | CborByteEncoderStream | CborTextEncoderStream | CborArrayEncoderStream From 8df2d62f64e8227ad280dd120dcf0656fe4c9548 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Tue, 19 May 2026 09:39:26 -0700 Subject: [PATCH 2/3] Address review: rename to ReadonlyCborType, remove pointless test comments Per @BlackAsLight on #7148: - Rename CborInputType -> ReadonlyCborType. Encoder signature now reads encodeCbor(value: CborType | ReadonlyCborType): Uint8Array (and the equivalent in encodeCborSequence), which keeps CborType prominent in the public API and clearly documents what additional shapes the encoder accepts. - Drop the descriptive comments on the new tests. The test names already describe what they cover, and the comments would lose context over time. --- cbor/_common_encode.ts | 16 ++++++++-------- cbor/encode_cbor.ts | 11 ++++++----- cbor/encode_cbor_sequence.ts | 11 ++++++----- cbor/encode_cbor_sequence_test.ts | 1 - cbor/encode_cbor_test.ts | 6 ------ cbor/tag.ts | 6 +++--- cbor/types.ts | 14 +++++++------- 7 files changed, 30 insertions(+), 35 deletions(-) diff --git a/cbor/_common_encode.ts b/cbor/_common_encode.ts index 4b7d9e93b3f1..ab8c299a67cd 100644 --- a/cbor/_common_encode.ts +++ b/cbor/_common_encode.ts @@ -1,12 +1,12 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { CborTag } from "./tag.ts"; -import type { CborInputType } from "./types.ts"; +import type { ReadonlyCborType } from "./types.ts"; // Narrows to ReadonlyMap (which TS's instanceof Map doesn't do on its own). function isReadonlyMap( x: unknown, -): x is ReadonlyMap { +): x is ReadonlyMap { return x instanceof Map; } @@ -27,7 +27,7 @@ function calcHeaderSize(x: number | bigint): number { return 9; } -export function calcEncodingSize(x: CborInputType): number { +export function calcEncodingSize(x: ReadonlyCborType): number { if (x == undefined || typeof x === "boolean") return 1; if (typeof x === "number") { return x % 1 === 0 ? calcHeaderSize(x < 0 ? -x - 1 : x) : 9; @@ -68,7 +68,7 @@ export function calcEncodingSize(x: CborInputType): number { } export function encode( - input: CborInputType, + input: ReadonlyCborType, output: Uint8Array, offset: number, ): number { @@ -210,7 +210,7 @@ function encodeString( } function encodeArray( - input: readonly CborInputType[], + input: readonly ReadonlyCborType[], output: Uint8Array, offset: number, ): number { @@ -220,7 +220,7 @@ function encodeArray( } function encodeObject( - input: { readonly [k: string]: CborInputType }, + input: { readonly [k: string]: ReadonlyCborType }, output: Uint8Array, offset: number, ): number { @@ -239,7 +239,7 @@ function encodeDate(input: Date, output: Uint8Array, offset: number): number { } function encodeTag( - input: CborTag, + input: CborTag, output: Uint8Array, offset: number, ): number { @@ -263,7 +263,7 @@ function encodeTag( } function encodeMap( - input: ReadonlyMap, + input: ReadonlyMap, output: Uint8Array, offset: number, ): number { diff --git a/cbor/encode_cbor.ts b/cbor/encode_cbor.ts index 00b9b606329d..3de64d8f7a26 100644 --- a/cbor/encode_cbor.ts +++ b/cbor/encode_cbor.ts @@ -1,11 +1,11 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { calcEncodingSize, encode } from "./_common_encode.ts"; -import type { CborInputType } from "./types.ts"; +import type { CborType, ReadonlyCborType } from "./types.ts"; /** - * Encodes a {@link CborInputType} value into a CBOR format represented as a - * {@link Uint8Array}. + * Encodes a {@link CborType} or {@link ReadonlyCborType} value into a CBOR + * format represented as a {@link Uint8Array}. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * * @example Usage @@ -32,10 +32,11 @@ import type { CborInputType } from "./types.ts"; * assertEquals(decodedMessage, rawMessage); * ``` * - * @param value The value to encode of type {@link CborInputType}. + * @param value The value to encode of type {@link CborType} or + * {@link ReadonlyCborType}. * @returns A {@link Uint8Array} representing the encoded data. */ -export function encodeCbor(value: CborInputType): Uint8Array { +export function encodeCbor(value: CborType | ReadonlyCborType): Uint8Array { const output = new Uint8Array(calcEncodingSize(value)); const o = encode(value, output, 0); if (o !== output.length) return output.subarray(0, o); diff --git a/cbor/encode_cbor_sequence.ts b/cbor/encode_cbor_sequence.ts index d757484220d2..e9051976ad8e 100644 --- a/cbor/encode_cbor_sequence.ts +++ b/cbor/encode_cbor_sequence.ts @@ -1,11 +1,11 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { calcEncodingSize, encode } from "./_common_encode.ts"; -import type { CborInputType } from "./types.ts"; +import type { CborType, ReadonlyCborType } from "./types.ts"; /** - * Encodes an array of {@link CborInputType} values into a CBOR format - * sequence represented as a {@link Uint8Array}. + * Encodes an array of {@link CborType} or {@link ReadonlyCborType} values + * into a CBOR format sequence represented as a {@link Uint8Array}. * [RFC 8949 - Concise Binary Object Representation (CBOR)](https://datatracker.ietf.org/doc/html/rfc8949) * * @example Usage @@ -31,11 +31,12 @@ import type { CborInputType } from "./types.ts"; * assertEquals(decodedMessage, rawMessage); * ``` * - * @param values An array of values to encode of type {@link CborInputType} + * @param values An array of values to encode, each of type {@link CborType} + * or {@link ReadonlyCborType}. * @returns A {@link Uint8Array} representing the encoded data. */ export function encodeCborSequence( - values: readonly CborInputType[], + values: readonly (CborType | ReadonlyCborType)[], ): Uint8Array { let o = 0; for (const value of values) o += calcEncodingSize(value); diff --git a/cbor/encode_cbor_sequence_test.ts b/cbor/encode_cbor_sequence_test.ts index 82bc41fdadd6..425f5309c62e 100644 --- a/cbor/encode_cbor_sequence_test.ts +++ b/cbor/encode_cbor_sequence_test.ts @@ -14,7 +14,6 @@ Deno.test("encodeCborSequence() correctly encoding", () => { Deno.test("encodeCborSequence() accepting readonly array input", () => { const values = [1, "two", { three: 3n }] as const; const mutable: CborType[] = [1, "two", { three: 3n }]; - // `readonly CborInputType[]` is now an accepted input. assertEquals( encodeCborSequence(values), encodeCborSequence(mutable), diff --git a/cbor/encode_cbor_test.ts b/cbor/encode_cbor_test.ts index 10b1d07fb4f7..4e408dd0e75f 100644 --- a/cbor/encode_cbor_test.ts +++ b/cbor/encode_cbor_test.ts @@ -500,9 +500,6 @@ Deno.test("encodeCbor() rejecting CborTag()", () => { }); Deno.test("encodeCbor() accepting `as const` (deeply readonly) input", () => { - // Regression test for #5831. Before, this call failed type-checking - // (`readonly` properties weren't assignable to `CborType`). The encoded - // bytes are identical to the mutable equivalent. const data = { a: 1, b: { c: 2n }, @@ -514,7 +511,6 @@ Deno.test("encodeCbor() accepting `as const` (deeply readonly) input", () => { Deno.test("encodeCbor() accepting readonly array literals", () => { const tuple = ["hello", 42, { nested: "value" }] as const; - // `readonly [3, ...]` is now assignable to the encoder input type. assertEquals( encodeCbor(tuple), encodeCbor(["hello", 42, { nested: "value" }]), @@ -528,7 +524,6 @@ Deno.test("encodeCbor() accepting ReadonlyMap input", () => { [[5], { a: 6 }], ]); - // `ReadonlyMap<...>` is now assignable to the encoder input type. assertEquals( encodeCbor(map), encodeCbor( @@ -548,7 +543,6 @@ Deno.test("encodeCbor() accepting readonly index signature", () => { Deno.test("encodeCbor() accepting CborTag with readonly content", () => { const data = [1, { inner: 2 }] as const; - // The tag content type widens to accept `CborInputType`. assertEquals( encodeCbor(new CborTag(1, data)), encodeCbor(new CborTag(1, structuredClone(data))), diff --git a/cbor/tag.ts b/cbor/tag.ts index 5e9936db0f08..ee3b7f4faf41 100644 --- a/cbor/tag.ts +++ b/cbor/tag.ts @@ -1,10 +1,10 @@ // Copyright 2018-2026 the Deno authors. MIT license. import type { - CborInputType, CborStreamInput, CborStreamOutput, CborType, + ReadonlyCborType, } from "./types.ts"; /** @@ -35,11 +35,11 @@ import type { * ``` * * @typeParam T The type of the tag's content, which can be a - * {@link CborType}, {@link CborInputType}, {@link CborStreamInput}, or + * {@link CborType}, {@link ReadonlyCborType}, {@link CborStreamInput}, or * {@link CborStreamOutput}. */ export class CborTag< - T extends CborType | CborInputType | CborStreamInput | CborStreamOutput, + T extends CborType | ReadonlyCborType | CborStreamInput | CborStreamOutput, > { /** * A {@link number} or {@link bigint} representing the CBOR tag number, used diff --git a/cbor/types.ts b/cbor/types.ts index 7a15a2af09c8..dc7d83cc03b4 100644 --- a/cbor/types.ts +++ b/cbor/types.ts @@ -29,7 +29,7 @@ export type CborPrimitiveType = * {@link encodeCbor}, {@link decodeCbor}, {@link encodeCborSequence}, and * {@link decodeCborSequence}. * - * The encoder functions also accept {@link CborInputType}, which permits + * The encoder functions also accept {@link ReadonlyCborType}, which permits * `readonly` arrays, `ReadonlyMap`, and `readonly` index signatures. */ export type CborType = @@ -42,7 +42,7 @@ export type CborType = }; /** - * Readonly-friendly input type for {@link encodeCbor} and + * Readonly variant of {@link CborType} accepted by {@link encodeCbor} and * {@link encodeCborSequence}. Lets you pass `as const` literals, * `ReadonlyMap`s, and frozen arrays without casting. * @@ -59,13 +59,13 @@ export type CborType = * encodeCbor(data); * ``` */ -export type CborInputType = +export type ReadonlyCborType = | CborPrimitiveType - | CborTag - | ReadonlyMap - | readonly CborInputType[] + | CborTag + | ReadonlyMap + | readonly ReadonlyCborType[] | { - readonly [k: string]: CborInputType; + readonly [k: string]: ReadonlyCborType; }; /** From 8bdcc18dac81133f93f94c37c9b03b1377d7ad47 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Tue, 26 May 2026 10:03:19 -0700 Subject: [PATCH 3/3] Address review: explain why CborType | ReadonlyCborType union is needed bartlomieju noted on #7148 that the union looks redundant at first glance because mutable Map/array/index-signature are all assignable to their readonly counterparts. The reason it is needed is that CborTag has a writable tagContent field and is therefore invariant in T, so CborTag is not assignable to CborTag and the union preserves existing CborTag callers. Document that on ReadonlyCborType so the union is not 'simplified' away later. --- cbor/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cbor/types.ts b/cbor/types.ts index dc7d83cc03b4..9a6530a84869 100644 --- a/cbor/types.ts +++ b/cbor/types.ts @@ -59,6 +59,13 @@ export type CborType = * encodeCbor(data); * ``` */ +// Note: encoder signatures take `CborType | ReadonlyCborType` rather than +// just `ReadonlyCborType`. Mutable `Map`/array/index-signature are all +// assignable to their readonly counterparts, so the union looks redundant, +// but `CborTag` has a writable `tagContent: T` field, which makes it +// invariant in `T`. That means `CborTag` is NOT assignable to +// `CborTag`, and existing callers passing +// `CborTag` instances would break if the union were collapsed. export type ReadonlyCborType = | CborPrimitiveType | CborTag