@absoluteui/helpers/http
Status reason phrases and messages for every status, failure categories, Problem Details (RFC 9457), header parsers, and response validation.
import { ... } from '@absoluteui/helpers/http';How to use
Section titled “How to use”The HTTP knowledge a client needs without the client: status classes and plain-language messages, RFC 9457 Problem Details, retry rules, and parsers for the headers you actually act on (Retry-After, Link, Content-Disposition, ETag, Cache-Control). Reach for it when you call fetch yourself or handle responses from another library. The HTTP client in @absoluteui/services/http (in preview) uses all of it; the HTTP guide covers the client, error categories, and translating status messages.
How do I turn a failed fetch into a message and field errors?
Section titled “How do I turn a failed fetch into a message and field errors?”assertResponse throws an HttpError for an unexpected status or content type (its message never contains the query or body). For a failure, read the Problem Details body: problemToFieldErrors understands the common shapes (RFC 9457 errors, invalid-params, ASP.NET, violations), and userMessageForStatus gives a message when the body has none.
import { assertResponse, parseProblemDetails, problemToFieldErrors, userMessageForStatus } from '@absoluteui/helpers/http';
const res = new Response( JSON.stringify({ title: 'Invalid leave request', status: 422, errors: [{ detail: 'Must be a working day.', pointer: '#/startDate' }] }), { status: 422, headers: { 'content-type': 'application/problem+json' } },);let fields: Record<string, string> = {};let message = '';try { assertResponse(res, { expectContentType: 'json' });} catch { const problem = parseProblemDetails(await res.text()); fields = problemToFieldErrors(problem); message = userMessageForStatus(res.status);}fields; // => { startDate: 'Must be a working day.' }message; // => 'Some of the information is not valid. Check it and try again.'How do I decide whether to retry, and when?
Section titled “How do I decide whether to retry, and when?”Retry only statuses that may succeed later, and only methods that are safe to repeat: a retried POST can create a second leave request. Honour Retry-After, which comes as seconds or an HTTP date.
import { isIdempotentMethod, isRetryableStatus, parseRetryAfter } from '@absoluteui/helpers/http';
const retryDelay = (method: string, status: number, retryAfter: string | null, attempt: number): number | undefined => { if (!isRetryableStatus(status) || !isIdempotentMethod(method) || attempt >= 3) return undefined; return parseRetryAfter(retryAfter, Date.now(), { maxMs: 30_000 }) ?? 500 * 2 ** attempt;};retryDelay('GET', 503, '5', 0); // => 5000retryDelay('GET', 429, null, 2); // => 2000retryDelay('POST', 503, '5', 0); // => undefined // not idempotentretryDelay('PUT', 404, null, 0); // => undefined // will not changeHow do I follow server-side pagination?
Section titled “How do I follow server-side pagination?”APIs that page with a Link header (rel="next") need no page arithmetic: getPaginationLinks resolves the targets against the request URL.
import { getPaginationLinks } from '@absoluteui/helpers/http';
const link = '</api/employees?cursor=b7Kq&limit=50>; rel="next", </api/employees?limit=50>; rel="first"';const pages = getPaginationLinks(link, 'https://hr.example.com/api/employees?limit=50');pages.next; // => 'https://hr.example.com/api/employees?cursor=b7Kq&limit=50'pages.prev; // => undefined // on the first pageHow do I save a download under the server’s file name?
Section titled “How do I save a download under the server’s file name?”parseContentDisposition decodes filename* (non-ASCII names) and gives safeFilename, a name that is safe on every operating system. Never write the raw filename: it can contain a path.
import { parseContentDisposition } from '@absoluteui/helpers/http';
const header = `attachment; filename="payslip.pdf"; filename*=UTF-8''Gehaltsabrechnung%20M%C3%BCller%202026-09.pdf`;parseContentDisposition(header)?.safeFilename; // => 'Gehaltsabrechnung Müller 2026-09.pdf'parseContentDisposition('attachment; filename="../../boot.ini"')?.safeFilename; // => 'boot.ini'How do I avoid overwriting someone else’s change?
Section titled “How do I avoid overwriting someone else’s change?”Keep the ETag from the load, send it as If-Match on save, and treat 412 as a conflict. etagMatchesAny is the same check for a server or a mock; strong comparison is required for If-Match.
import { etagMatchesAny, httpFailureCategory, httpStatus } from '@absoluteui/helpers/http';
const loadedEtag = '"v41"';const headers = { 'content-type': 'application/json', 'if-match': loadedEtag };etagMatchesAny(headers['if-match'], '"v42"', { weak: false }); // => false // someone saved in betweenhttpFailureCategory(httpStatus.PRECONDITION_FAILED); // => 'conflict'What goes wrong?
Section titled “What goes wrong?”isProblemDetails accepts any object with a well-typed type, title, or status, so it cannot tell your API’s errors from a proxy’s. Oversized error bodies are rejected before parsing. Log URLs with safeUrlForLog, which drops credentials, query, and fragment (tokens often travel there).
import { assertResponse, parseProblemDetails, safeUrlForLog } from '@absoluteui/helpers/http';
assertResponse(new Response('<html>', { headers: { 'content-type': 'text/html' } }), { expectContentType: 'json' }); // => throws HLP_HTTP_CONTENT_TYPEparseProblemDetails('{"title":"x"}'.padEnd(2000), { maxLength: 1000 }); // => throws HLP_HTTP_PROBLEM_TOO_LARGEparseProblemDetails('{"message":"Oops"}'); // => undefined // not problem detailssafeUrlForLog('https://hr.example.com/reset?token=s3cr3t'); // => 'https://hr.example.com/reset'API reference
Section titled “API reference”Headers
Section titled “Headers”Functions
Section titled “Functions”decodeExtValue
Decodes an RFC 8187 / RFC 5987 ext-value such as UTF-8''na%C3%AFve.txt. Supports UTF-8
and ISO-8859-1; returns undefined for other charsets or malformed encoding.
function decodeExtValue(value: string): string | undefined;Example:
decodeExtValue("UTF-8''%E2%82%AC%20rates") // => '€ rates'etagMatches
Compares entity tags (RFC 9110 the specification). Weak comparison (default) ignores the W/ flag;
strong comparison requires both to be strong. Unparseable tags never match.
function etagMatches(a: string | ETag | null | undefined, b: string | ETag | null | undefined, options?: { readonly weak?: boolean;}): boolean;Example:
etagMatches('W/"1"', '"1"') // => trueExample:
etagMatches('W/"1"', '"1"', { weak: false }) // => falseetagMatchesAny
True when etag matches an If-None-Match (weak) or If-Match (weak: false) header.
function etagMatchesAny(header: string | null | undefined, etag: string | ETag | null | undefined, options?: { readonly weak?: boolean;}): boolean;Example:
etagMatchesAny('"a", "b"', '"b"') // => truegetPaginationLinks
Extracts next, prev (or previous), first and last targets from a Link header.
function getPaginationLinks(header: string | readonly Link[] | null | undefined, baseUrl?: string): PaginationLinks;Example:
getPaginationLinks('<?page=3>; rel="next", <?page=1>; rel="prev"', 'https://api.example.com/items') // => { next: 'https://api.example.com/items?page=3', prev: 'https://api.example.com/items?page=1' }isJsonContentType
True for application/json and any +json media type (application/problem+json).
function isJsonContentType(value: string | null | undefined): boolean;Example:
isJsonContentType('application/vnd.api+json') // => trueparseAcceptLanguage
Parses Accept-Language, drops q=0 and malformed entries, and sorts by quality
(stable for ties). At most max entries (default 50) are read.
function parseAcceptLanguage(value: string | null | undefined, max?: number): LanguageRange[];Example:
parseAcceptLanguage('fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5') // => [{ tag: 'fr-CH', q: 1 }, { tag: 'fr', q: 0.9 }, { tag: 'en', q: 0.8 }, { tag: '*', q: 0.5 }]parseCacheControl
Parses Cache-Control (RFC 9111). Invalid delta-seconds are ignored; values over 2^31 are capped.
function parseCacheControl(value: string | null | undefined): CacheControl;Example:
parseCacheControl('public, max-age=3600, stale-while-revalidate=60').maxAge // => 3600parseContentDisposition
Parses Content-Disposition (RFC 6266), decoding filename* (RFC 8187) and producing a
sanitized file name candidate for saving.
function parseContentDisposition(value: string | null | undefined): ContentDisposition | undefined;Example:
parseContentDisposition(`attachment; filename="plain.txt"; filename*=UTF-8''%E2%82%AC.txt`)?.filename // => '€.txt'parseContentType
Parses a Content-Type value.
function parseContentType(value: string | null | undefined): ContentType | undefined;Example:
parseContentType('application/json; charset=UTF-8') // => { type: 'application', subtype: 'json', suffix: '', mediaType: 'application/json', params: { charset: 'utf-8' } }parseETag
Parses one entity tag ("abc" or W/"abc"), RFC 9110 the specification
function parseETag(value: string | null | undefined): ETag | undefined;Example:
parseETag('W/"v1"') // => { weak: true, tag: 'v1' }parseETagList
Parses an If-Match / If-None-Match list: '*' or entity tags (invalid members skipped).
function parseETagList(value: string | null | undefined): '*' | ETag[];Example:
parseETagList('"a", W/"b"') // => [{ weak: false, tag: 'a' }, { weak: true, tag: 'b' }]parseHttpDate
Parses an HTTP-date (RFC 9110 the specification): IMF-fixdate, obsolete RFC 850 and asctime formats.
Returns epoch milliseconds or undefined.
function parseHttpDate(value: string | null | undefined, now?: number | Date): number | undefined;Example:
parseHttpDate('Sun, 06 Nov 1994 08:49:37 GMT') // => 784111777000parseLinkHeader
Parses a Link header (RFC 8288): multiple links, quoted parameters (commas and semicolons
inside quotes), multiple relation types, and RFC 8187 title*. Malformed links are skipped.
function parseLinkHeader(value: string | null | undefined, options?: LinkHeaderOptions): Link[];Example:
parseLinkHeader('<https://api.example.com/items?page=2>; rel="next"')[0]?.rel // => ['next']parseRetryAfter
Converts Retry-After (delta-seconds or HTTP-date) to a delay in milliseconds, clamped to
0...maxMs (default one hour). Returns undefined when absent or invalid.
function parseRetryAfter(value: string | null | undefined, now?: number | Date, options?: { readonly maxMs?: number;}): number | undefined;Example:
parseRetryAfter('120') // => 120000Example:
parseRetryAfter('Wed, 21 Oct 2015 07:28:00 GMT', Date.UTC(2015, 9, 21, 7, 27, 0)) // => 60000Interfaces
Section titled “Interfaces”CacheControl
interface CacheControl { readonly maxAge?: number; readonly sMaxAge?: number; readonly staleWhileRevalidate?: number; readonly staleIfError?: number; readonly maxStale?: number; readonly minFresh?: number; readonly noCache: boolean; readonly noStore: boolean; readonly noTransform: boolean; readonly mustRevalidate: boolean; readonly proxyRevalidate: boolean; readonly mustUnderstand: boolean; readonly private: boolean; readonly public: boolean; readonly immutable: boolean; readonly onlyIfCached: boolean; readonly directives: Readonly<Record<string, string | true>>;}ContentDisposition
interface ContentDisposition { readonly type: string; readonly filename: string | undefined; readonly safeFilename: string | undefined; readonly params: Readonly<Record<string, string>>;}ContentType
interface ContentType { readonly type: string; readonly subtype: string; readonly suffix: string; readonly mediaType: string; readonly params: Readonly<Record<string, string>>;}ETag
interface ETag { readonly weak: boolean; readonly tag: string;}LanguageRange
interface LanguageRange { readonly tag: string; readonly q: number;}Link
interface Link { readonly uri: string; readonly rel: readonly string[]; readonly params: Readonly<Record<string, string>>;}LinkHeaderOptions
interface LinkHeaderOptions { readonly baseUrl?: string; readonly maxLinks?: number; readonly maxLength?: number;}PaginationLinks
interface PaginationLinks { readonly next?: string; readonly prev?: string; readonly first?: string; readonly last?: string;}Problem
Section titled “Problem”Functions
Section titled “Functions”isProblemDetails
True for an object that looks like RFC 9457 problem details: at least one of type,
title or status, and every standard member present has the right type.
function isProblemDetails(value: unknown): value is Record<string, unknown>;Example:
isProblemDetails({ title: 'Out of credit', status: 403 }) // => trueparseProblemDetails
Parses RFC 9457 problem details from a JSON string or an already-parsed value. Returns
undefined when the input is not a problem-details object. JSON text over maxLength
(default 1 000 000) throws HLP_HTTP_PROBLEM_TOO_LARGE.
function parseProblemDetails(input: unknown, options?: { readonly maxLength?: number;}): ProblemDetails | undefined;Example:
parseProblemDetails('{"type":"https://example.com/probs/out-of-credit","title":"You do not have enough credit.","status":403,"balance":30}')// { type: 'https://example.com/probs/out-of-credit', title: '…', status: 403, extensions: { balance: 30 }, fieldErrors: {} }problemToFieldErrors
Extracts field errors from common problem-details extensions: invalid-params
([{ name, reason }], RFC 7807), errors as [{ detail, pointer }] (RFC 9457 example),
[{ field, message }], or { field: string | string[] } (ASP.NET), and violations
([{ field, message }]). Pointers become paths like items[2].qty; first message per field.
function problemToFieldErrors(problem: unknown, maxFields?: number): Record<string, string>;Example:
problemToFieldErrors({ errors: [{ detail: 'must be positive', pointer: '#/age' }] }) // => { age: 'must be positive' }Interfaces
Section titled “Interfaces”ProblemDetails
Normalized problem details. Members with the wrong JSON type are ignored (RFC 9457 the specification).
interface ProblemDetails { readonly type: string; readonly title?: string; readonly status?: number; readonly detail?: string; readonly instance?: string; readonly extensions: Readonly<Record<string, unknown>>; readonly fieldErrors: Readonly<Record<string, string>>;}Related types
Section titled “Related types”Functions
Section titled “Functions”httpFailureCategory
Failure category of a status: specific statuses first, then client (4xx) or server (5xx).
function httpFailureCategory(status: number): HttpFailureCategory;Example:
httpFailureCategory(422) // => 'validation'Example:
httpFailureCategory(503) // => 'unavailable'Example:
httpFailureCategory(418) // => 'client'HttpFailureCategory
Why an HTTP call failed, for choosing a message, a retry, or a sign-in flow. client covers
the 4xx statuses without a more specific category.
type HttpFailureCategory = 'network' | 'offline' | 'timeout' | 'cancelled' | 'unauthorized' | 'forbidden' | 'not-found' | 'conflict' | 'validation' | 'rate-limited' | 'client' | 'server' | 'unavailable' | 'bad-response' | 'too-large';Response
Section titled “Response”Functions
Section titled “Functions”assertResponse
Throws HttpError unless the response has an expected status (HLP_HTTP_STATUS) and content
type (HLP_HTTP_CONTENT_TYPE). Messages never contain the URL query or body. 204, 205 and
304 responses skip the content-type check.
function assertResponse(response: ResponseLike, options?: AssertResponseOptions): void;Example:
const res = new Response('{"id":7}', { headers: { 'content-type': 'application/json' } });assertResponse(res, { expectContentType: 'json' });const data: unknown = await res.json(); // => { id: 7 }assertResponse(new Response(null, { status: 404 })); // => throws HttpErrorclassifyHttpError
Classifies anything thrown by a fetch-based call.
function classifyHttpError(error: unknown): HttpErrorCategory;Example:
classifyHttpError(new TypeError('Failed to fetch')) // => 'network'Example:
import { HttpError } from '@absoluteui/helpers/error';classifyHttpError(new HttpError('X', 'Failed.', { status: 401 })); // => 'auth'safeUrlForLog
Strips credentials, query and fragment from a URL so it is safe to log.
function safeUrlForLog(url: string): string;Example:
safeUrlForLog('https://u:p@api.example.com/a?token=x#f') // => 'https://api.example.com/a'Interfaces
Section titled “Interfaces”AssertResponseOptions
interface AssertResponseOptions { readonly expectStatus?: number | readonly number[] | ((status: number) => boolean); readonly expectContentType?: 'json' | (string & {}) | readonly string[]; readonly method?: string;}ResponseLike
Minimal response shape (satisfied by the Fetch API Response).
interface ResponseLike { readonly status: number; readonly statusText?: string; readonly url?: string; readonly headers: { get(name: string): string | null; };}HttpErrorCategory
Coarse error category for choosing a message, retry or sign-in flow.
type HttpErrorCategory = 'network' | 'timeout' | 'aborted' | 'client' | 'server' | 'auth' | 'unknown';Status
Section titled “Status”Functions
Section titled “Functions”composeStatusMessages
One catalog from several; the first that defines a status wins.
function composeStatusMessages(...catalogs: readonly (StatusMessageCatalog | undefined)[]): StatusMessageCatalog;Example:
const app = composeStatusMessages(statusMessageCatalog({ 404: 'No such employee.' }), statusMessageCatalog({ 404: 'Not here.', 409: 'Someone else saved first.' }));[app(404), app(409), app(500)]; // => ['No such employee.', 'Someone else saved first.', undefined]describeStatus
Reason phrase, message, and class of a status.
function describeStatus(status: number, catalog?: StatusMessageCatalog): StatusDescription;Example:
describeStatus(429);// => { status: 429, reason: 'Too Many Requests', message: 'Too many requests. Wait a moment and try again.', statusClass: 'client-error' }isClientError
4xx.
function isClientError(status: number): boolean;Example:
isClientError(404) // => trueisIdempotentMethod
RFC 9110 the specification idempotent methods: GET, HEAD, OPTIONS, TRACE, PUT, DELETE (case-insensitive).
function isIdempotentMethod(method: string): boolean;Example:
isIdempotentMethod('put') // => trueisInformational
1xx.
function isInformational(status: number): boolean;Example:
isInformational(103) // => trueisRedirect
3xx.
function isRedirect(status: number): boolean;Example:
isRedirect(302) // => trueisRetryableStatus
True when a request that failed with status may succeed if retried. Combine with
{@link isIdempotentMethod}: never blindly retry a POST.
function isRetryableStatus(status: number, retryable?: ReadonlySet<number> | readonly number[]): boolean;Example:
isRetryableStatus(503) // => trueisSafeMethod
RFC 9110 the specification safe (read-only) methods: GET, HEAD, OPTIONS, TRACE.
function isSafeMethod(method: string): boolean;Example:
isSafeMethod('POST') // => falseisServerError
5xx.
function isServerError(status: number): boolean;Example:
isServerError(503) // => trueisSuccess
2xx.
function isSuccess(status: number): boolean;Example:
isSuccess(204) // => truelocalizedStatusMessages
Catalog for a locale from per-locale message objects, falling back from the full tag to its
language (fr-CA -> fr). Missing entries fall through to the English defaults.
function localizedStatusMessages(catalogs: Readonly<Record<string, StatusMessages>>, locale: string): StatusMessageCatalog;Example:
const catalog = localizedStatusMessages({ fr: { 404: 'Élément introuvable.' } }, 'fr-CA');userMessageForStatus(404, catalog); // => 'Élément introuvable.'userMessageForStatus(503, catalog); // => 'The service is temporarily unavailable. Try again in a few minutes.'statusClass
Status class of a code.
function statusClass(status: number): StatusClass;Example:
statusClass(0) // => 'network'Example:
statusClass(422) // => 'client-error'statusMessageCatalog
Catalog from a messages object.
function statusMessageCatalog(messages: StatusMessages): StatusMessageCatalog;Example:
const catalog = statusMessageCatalog({ 404: 'No such employee.' });[catalog(404), catalog(500)]; // => ['No such employee.', undefined]statusText
Standard reason phrase, or '' for unregistered codes.
function statusText(status: number): string;Example:
statusText(404) // => 'Not Found'userMessageForStatus
Plain-language message for a status (0 = network failure). catalog entries win; then
{@link DEFAULT_STATUS_MESSAGES}; then a message for the status class.
function userMessageForStatus(status: number, catalog?: StatusMessageCatalog): string;Example:
userMessageForStatus(503) // => 'The service is temporarily unavailable. Try again in a few minutes.'Example:
userMessageForStatus(404, (s) => (s === 404 ? 'No such employee.' : undefined)); // => 'No such employee.'userMessageForStatus(499); // => 'The request could not be completed. Check the information and try again.'Constants
Section titled “Constants”DEFAULT_RETRYABLE_STATUSES
Statuses retried by default: 408, 425, 429, 500, 502, 503, 504.
const DEFAULT_RETRYABLE_STATUSES: ReadonlySet<number>;Example:
DEFAULT_RETRYABLE_STATUSES.has(503) // => trueDEFAULT_STATUS_MESSAGES
Plain-language English message for every registered status code, plus 0 (network failure),
for alerts, toasts, and error summaries. Frozen; copy it as the starting point of a translation.
const DEFAULT_STATUS_MESSAGES: StatusMessages;Example:
DEFAULT_STATUS_MESSAGES[404] // => 'The item could not be found. It may have been moved or deleted.'Example:
Object.keys(DEFAULT_STATUS_MESSAGES).length // => 63httpStatus
Named status codes (RFC 9110 and registered extensions).
const httpStatus: Readonly<{ readonly CONTINUE: 100; readonly SWITCHING_PROTOCOLS: 101; readonly PROCESSING: 102; readonly EARLY_HINTS: 103; readonly OK: 200; readonly CREATED: 201; readonly ACCEPTED: 202; readonly NON_AUTHORITATIVE_INFORMATION: 203; readonly NO_CONTENT: 204; readonly RESET_CONTENT: 205; readonly PARTIAL_CONTENT: 206; readonly MULTI_STATUS: 207; readonly ALREADY_REPORTED: 208; readonly IM_USED: 226; readonly MULTIPLE_CHOICES: 300; readonly MOVED_PERMANENTLY: 301; readonly FOUND: 302; readonly SEE_OTHER: 303; readonly NOT_MODIFIED: 304; readonly USE_PROXY: 305; readonly TEMPORARY_REDIRECT: 307; readonly PERMANENT_REDIRECT: 308; readonly BAD_REQUEST: 400; readonly UNAUTHORIZED: 401; readonly PAYMENT_REQUIRED: 402; readonly FORBIDDEN: 403; readonly NOT_FOUND: 404; readonly METHOD_NOT_ALLOWED: 405; readonly NOT_ACCEPTABLE: 406; readonly PROXY_AUTHENTICATION_REQUIRED: 407; readonly REQUEST_TIMEOUT: 408; readonly CONFLICT: 409; readonly GONE: 410; readonly LENGTH_REQUIRED: 411; readonly PRECONDITION_FAILED: 412; readonly CONTENT_TOO_LARGE: 413; readonly URI_TOO_LONG: 414; readonly UNSUPPORTED_MEDIA_TYPE: 415; readonly RANGE_NOT_SATISFIABLE: 416; readonly EXPECTATION_FAILED: 417; readonly IM_A_TEAPOT: 418; readonly MISDIRECTED_REQUEST: 421; readonly UNPROCESSABLE_CONTENT: 422; readonly LOCKED: 423; readonly FAILED_DEPENDENCY: 424; readonly TOO_EARLY: 425; readonly UPGRADE_REQUIRED: 426; readonly PRECONDITION_REQUIRED: 428; readonly TOO_MANY_REQUESTS: 429; readonly REQUEST_HEADER_FIELDS_TOO_LARGE: 431; readonly UNAVAILABLE_FOR_LEGAL_REASONS: 451; readonly INTERNAL_SERVER_ERROR: 500; readonly NOT_IMPLEMENTED: 501; readonly BAD_GATEWAY: 502; readonly SERVICE_UNAVAILABLE: 503; readonly GATEWAY_TIMEOUT: 504; readonly HTTP_VERSION_NOT_SUPPORTED: 505; readonly VARIANT_ALSO_NEGOTIATES: 506; readonly INSUFFICIENT_STORAGE: 507; readonly LOOP_DETECTED: 508; readonly NOT_EXTENDED: 510; readonly NETWORK_AUTHENTICATION_REQUIRED: 511;}>;Example:
httpStatus.NOT_FOUND // => 404Interfaces
Section titled “Interfaces”StatusDescription
Everything a UI needs to describe a status.
interface StatusDescription { readonly status: number; readonly reason: string; readonly message: string; readonly statusClass: StatusClass;}HttpStatusName
type HttpStatusName = keyof typeof httpStatus;StatusClass
Status class of a code; network for 0 (no response).
type StatusClass = 'network' | 'informational' | 'success' | 'redirect' | 'client-error' | 'server-error' | 'unknown';StatusMessageCatalog
Replacement messages by status; return undefined to fall back to the defaults.
type StatusMessageCatalog = (status: number) => string | undefined;StatusMessages
Messages keyed by status code, for translation files and overrides.
type StatusMessages = Readonly<Partial<Record<number, string>>>;