From 6dbfabb2fbfea87c361a39432eafb6addb577a7c Mon Sep 17 00:00:00 2001 From: Hashim Khan Date: Mon, 27 Jul 2026 00:36:34 +0500 Subject: [PATCH] fix(types): tighten parseCookie return type to Record Fixes #252 --- src/index.ts | 28 ++++++++++++++++++++-------- src/parse-cookie.spec.ts | 24 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 23fe8ea..a94ac00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,10 +79,15 @@ const NullObject = /* @__PURE__ */ (() => { return C; })() as unknown as { new (): any }; +/** + * Cookie value decoder. + */ +export type Decoder = (str: string) => unknown; + /** * Parse options. */ -export interface ParseOptions { +export interface ParseOptions string> { /** * Specifies a function that will be used to decode a [cookie-value](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1). * Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to decode @@ -95,13 +100,17 @@ export interface ParseOptions { * * @default decode */ - decode?: (str: string) => string | undefined; + decode?: Dec; } /** * Cookies object. + * + * With the default decoder, values are always strings. When a custom `decode` + * can return other types (including `undefined`), the return type of + * `parseCookie` mirrors that decoder via generics. */ -export type Cookies = Record; +export type Cookies = Record; /** * Parse a `Cookie` header. @@ -109,13 +118,16 @@ export type Cookies = Record; * Parse the given cookie header string into an object * The object has the various cookies as keys(names) => values */ -export function parseCookie(str: string, options?: ParseOptions): Cookies { - const obj: Cookies = new NullObject(); +export function parseCookie string>( + str: string, + options?: ParseOptions, +): Cookies> { + const obj: Cookies> = new NullObject(); const len = str.length; // RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='. if (len < 2) return obj; - const dec = options?.decode || decode; + const dec = (options?.decode || decode) as Dec; let index = 0; do { @@ -134,7 +146,7 @@ export function parseCookie(str: string, options?: ParseOptions): Cookies { // only assign once if (obj[key] === undefined) { - obj[key] = dec(valueSlice(str, eqIdx + 1, endIdx)); + obj[key] = dec(valueSlice(str, eqIdx + 1, endIdx)) as ReturnType; } index = endIdx + 1; @@ -157,7 +169,7 @@ export interface StringifyOptions { * Stringifies an object into an HTTP `Cookie` header. */ export function stringifyCookie( - cookie: Cookies, + cookie: Cookies, options?: StringifyOptions, ): string { const enc = options?.encode || defaultEncode; diff --git a/src/parse-cookie.spec.ts b/src/parse-cookie.spec.ts index e7bfc74..0c58819 100644 --- a/src/parse-cookie.spec.ts +++ b/src/parse-cookie.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, expectTypeOf } from "vitest"; import * as cookie from "./index.js"; import top from "../scripts/top-cookie.json" with { type: "json" }; @@ -122,4 +122,26 @@ describe("cookie.parseCookie", function () { expect(calls).toBe(2); }); }); + + describe("types", function () { + it("should return Record with the default decode", function () { + const result = cookie.parseCookie("foo=bar"); + expectTypeOf(result).toEqualTypeOf>(); + expectTypeOf(result.foo).toEqualTypeOf(); + }); + + it("should mirror a custom decode return type", function () { + const withNumber = cookie.parseCookie("foo=1", { + decode: (value: string) => Number(value), + }); + expectTypeOf(withNumber).toEqualTypeOf>(); + + const withOptional = cookie.parseCookie("foo=bar", { + decode: (value: string) => (value === "bar" ? undefined : value), + }); + expectTypeOf(withOptional).toEqualTypeOf< + Record + >(); + }); + }); });