#URLSearchParams
createURLSearchParamsCodec 把 Search Fields 变成可直接使用的 URLSearchParams codec。
import { createURLSearchParamsCodec } from '@guanriyue/decurl/codec';
const codec = createURLSearchParamsCodec(searchFields);它提供两个方法:
codec.decode(searchParams);
codec.encode(values, options);大多数情况下,开发者不需要主动调用 decode。日常业务通常会通过更上层的状态读取 API 获取 values。
但 encode 仍然很重要。当你需要构造链接、跳转目标、分享 URL、测试 fixture,或者在非 React Router 环境中生成 search string 时,encode 可以复用同一份 Search Fields,避免手写 query string。
#Encode
#类型安全的 search
encode 的输入来自 Search Fields。字段名和字段值都会被 TypeScript 检查。
import { const createURLSearchParamsCodec: <TDefinition extends RecordCodec>(definition: TDefinition) => URLSearchParamsCodec<TDefinition> createURLSearchParamsCodec , const defineFields: <TDefinition extends RecordCodec>(definition: TDefinition) => DefinedFields<TDefinition> defineFields , function field<TValue>(definition: SingleOptionalFieldCodec<TValue> & {
name: DefinedFieldName;
}): WithDefinedFieldName<SingleOptionalFieldCodec<TValue>> (+7 overloads)
field } from '@guanriyue/decurl/codec';
import { function elementOf<const TValues extends readonly unknown[]>(values: TValues): Decode<unknown, TValues[number]> (+1 overload) elementOf , const min: (minimum: number) => Decode<number, number> min , function pipe<A, B>(ab: Decode<A, B>): Decode<A, B> (+9 overloads) pipe , const shape: Shape shape , const toNumber: Decode<unknown, number> toNumber , module trim
const trim: {
(input: string): string;
start(input: string): string;
end(input: string): string;
}
trim } from '@guanriyue/decurl/decode';
const const searchFields: {
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}
searchFields = defineFields<{
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: SingleRequiredFieldCodec<number>;
sort: SingleRequiredFieldCodec<"relevance" | "latest">;
}>(definition: {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
page: SingleRequiredFieldCodec<number>;
sort: SingleRequiredFieldCodec<"relevance" | "latest">;
}): {
...;
}
defineFields ({
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>> keyword : field<string>(definition: FieldCodecBase<string> & {
mode?: "single";
decode: Decode<string, string>;
encode?: ((value: string) => string | null | undefined) | undefined;
defaultValue?: never;
} & {
name: DefinedFieldName;
}): WithDefinedFieldName<SingleOptionalFieldCodec<string>> (+7 overloads)
field ({
name: (FieldName | undefined) & DefinedFieldName name : 'q',
decode: Decode<string, string> decode : module trim
const trim: {
(input: string): string;
start(input: string): string;
end(input: string): string;
}
trim ,
}),
page: SingleRequiredFieldCodec<number> page : field<number>(definition: SingleRequiredFieldCodec<number>): SingleRequiredFieldCodec<number> (+7 overloads) field ({
decode: Decode<string, number> decode : pipe<string, string, string, number, number>(ab: Decode<string, string>, bc: Decode<string, string>, cd: Decode<string, number>, de: Decode<number, number>): Decode<string, number> (+9 overloads) pipe (module trim
const trim: {
(input: string): string;
start(input: string): string;
end(input: string): string;
}
trim , const shape: Shape shape .integer: Decode<string, string> integer , const toNumber: Decode<unknown, number> toNumber , function min(minimum: number): Decode<number, number> min (1)),
defaultValue: number defaultValue : 1,
}),
sort: SingleRequiredFieldCodec<"relevance" | "latest"> sort : field<"relevance" | "latest">(definition: SingleRequiredFieldCodec<"relevance" | "latest">): SingleRequiredFieldCodec<"relevance" | "latest"> (+7 overloads) field ({
decode: Decode<string, "relevance" | "latest"> decode : elementOf<readonly ["relevance", "latest"]>(values: readonly ["relevance", "latest"]): Decode<unknown, "relevance" | "latest"> (+1 overload) elementOf (['relevance', 'latest']),
defaultValue: NonNullable<"relevance" | "latest"> defaultValue : 'relevance',
}),
});
const const codec: URLSearchParamsCodec<{
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}>
codec = createURLSearchParamsCodec<{
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}>(definition: {
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}): URLSearchParamsCodec<...>
createURLSearchParamsCodec (const searchFields: {
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}
searchFields );
const const search: URLSearchParams search = const codec: URLSearchParamsCodec<{
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
page: WithDefinedFieldName<SingleRequiredFieldCodec<number>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}>
codec .encode: (values: Partial<{
keyword: string | null | undefined;
page: number | null | undefined;
sort: NonNullable<"relevance" | "latest" | null | undefined> | null | undefined;
}>, options?: EncodeFieldsOptions) => URLSearchParams
encode ({
keyword?: string | null | undefined keyword : 'router',
page?: number | null | undefined page : 2,
sort?: NonNullable<"relevance" | "latest" | null | undefined> | null | undefined sort : 'latest',
});
const search: URLSearchParams search .URLSearchParams.toString(): string toString ();
// q=router&page=2&sort=latest这比手写 query string 更可维护:
`?q=${keyword}&page=${page}&sort=${sort}`;手写字符串会绕过 Search Fields:key 是否正确、value 是否需要 encode、默认值是否应该省略,都需要调用方自己记住。
类型错误会在调用点暴露出来:
import { const createURLSearchParamsCodec: <TDefinition extends RecordCodec>(definition: TDefinition) => URLSearchParamsCodec<TDefinition> createURLSearchParamsCodec , const defineFields: <TDefinition extends RecordCodec>(definition: TDefinition) => DefinedFields<TDefinition> defineFields , function field<TValue>(definition: SingleOptionalFieldCodec<TValue> & {
name: DefinedFieldName;
}): WithDefinedFieldName<SingleOptionalFieldCodec<TValue>> (+7 overloads)
field } from '@guanriyue/decurl/codec';
import { function elementOf<const TValues extends readonly unknown[]>(values: TValues): Decode<unknown, TValues[number]> (+1 overload) elementOf , module trim
const trim: {
(input: string): string;
start(input: string): string;
end(input: string): string;
}
trim } from '@guanriyue/decurl/decode';
const const searchFields: {
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}
searchFields = defineFields<{
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
sort: SingleRequiredFieldCodec<"relevance" | "latest">;
}>(definition: {
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>>;
sort: SingleRequiredFieldCodec<"relevance" | "latest">;
}): {
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<...>;
}
defineFields ({
keyword: WithDefinedFieldName<SingleOptionalFieldCodec<string>> keyword : field<string>(definition: FieldCodecBase<string> & {
mode?: "single";
decode: Decode<string, string>;
encode?: ((value: string) => string | null | undefined) | undefined;
defaultValue?: never;
} & {
name: DefinedFieldName;
}): WithDefinedFieldName<SingleOptionalFieldCodec<string>> (+7 overloads)
field ({
name: (FieldName | undefined) & DefinedFieldName name : 'q',
decode: Decode<string, string> decode : module trim
const trim: {
(input: string): string;
start(input: string): string;
end(input: string): string;
}
trim ,
}),
sort: SingleRequiredFieldCodec<"relevance" | "latest"> sort : field<"relevance" | "latest">(definition: SingleRequiredFieldCodec<"relevance" | "latest">): SingleRequiredFieldCodec<"relevance" | "latest"> (+7 overloads) field ({
decode: Decode<string, "relevance" | "latest"> decode : elementOf<readonly ["relevance", "latest"]>(values: readonly ["relevance", "latest"]): Decode<unknown, "relevance" | "latest"> (+1 overload) elementOf (['relevance', 'latest']),
defaultValue: NonNullable<"relevance" | "latest"> defaultValue : 'relevance',
}),
});
const const codec: URLSearchParamsCodec<{
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}>
codec = createURLSearchParamsCodec<{
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}>(definition: {
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}): URLSearchParamsCodec<...>
createURLSearchParamsCodec (const searchFields: {
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}
searchFields );
const codec: URLSearchParamsCodec<{
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}>
codec .encode: (values: Partial<{
keyword: string | null | undefined;
sort: NonNullable<"relevance" | "latest" | null | undefined> | null | undefined;
}>, options?: EncodeFieldsOptions) => URLSearchParams
encode ({
keyword?: string | null | undefined keyword : 'router',
sort: 'oldest',Type '"oldest"' is not assignable to type 'NonNullable<"relevance" | "latest" | null | undefined> | null | undefined'.});
const codec: URLSearchParamsCodec<{
keyword: WithDefinedFieldName<WithDefinedFieldName<SingleOptionalFieldCodec<string>>>;
sort: WithDefinedFieldName<SingleRequiredFieldCodec<"relevance" | "latest">>;
}>
codec .encode: (values: Partial<{
keyword: string | null | undefined;
sort: NonNullable<"relevance" | "latest" | null | undefined> | null | undefined;
}>, options?: EncodeFieldsOptions) => URLSearchParams
encode ({
typo: 'router',Object literal may only specify known properties, and 'typo' does not exist in type 'Partial<{ keyword: string | null | undefined; sort: NonNullable<"relevance" | "latest" | null | undefined> | null | undefined; }>'.});#Patch encode
encode 默认是 patch 语义。
const nextSearch = codec.encode(
{ page: 3 },
{ base: '?q=router&page=2&sort=latest' },
);
nextSearch.toString();
// q=router&sort=latest&page=3Patch encode 的规则:
- 只处理 patch 中出现的 Search Fields field。
- patch 中不存在的 field 不会被修改。
- patch 中不属于 Search Fields 的 key 会被忽略。
null或undefined表示删除对应 URL key。
codec.encode(
{ keyword: undefined },
{ base: '?q=router&page=2' },
).toString();
// page=2这不是 full update。URL search params 常常被多个组件或功能共享,patch encode 更适合局部更新。
#Base search
base 表示当前 URLSearchParams。传入 base 时,encode 会从它的拷贝开始,再叠加 patch。
codec.encode(
{ page: 2 },
{ base: new URLSearchParams('?q=router&debug=true&utm_source=docs') },
).toString();
// q=router&debug=true&utm_source=docs&page=2这对真实页面很重要,因为 URL 里可能有当前组件不负责的参数。Search Fields 只更新自己认识的字段,其余参数会被保留。
#写回规则
#Canonical key
当 field 有多个 name 时,第一项是 canonical key,后续项是 legacy alias。
const searchFields = defineFields({
page: field({
name: ['page', 'p'],
decode: pipe(shape.integer, toNumber, min(1)),
defaultValue: 1,
}),
});写回时总是使用 canonical key。
const codec = createURLSearchParamsCodec(searchFields);
codec.encode(
{ page: 3 },
{ base: '?p=2' },
).toString();
// page=3只要 patch 触碰该 field,encode 就会清理 legacy alias,避免新旧 key 同时留在 URL 中。
旧链接: ?p=2
新写回: ?page=3#Default value
写入等于 defaultValue 的值时,默认会删除对应 URL key,让 URL 保持简洁。
const searchFields = defineFields({
page: field({
decode: pipe(shape.integer, toNumber, min(1)),
defaultValue: 1,
}),
});
const codec = createURLSearchParamsCodec(searchFields);
codec.encode({ page: 1 }).toString();
// ''如果希望默认值也出现在 URL 中,可以传入 preserveDefault: true。
codec.encode({ page: 1 }, { preserveDefault: true }).toString();
// page=1preserveDefault 是 encode 选项,不是 FieldCodec 属性。
#Multi field
Multi field 会按数组顺序写入多个同名 key。
import { mapItems, pipe, unique } from '@guanriyue/decurl/decode';
const searchFields = defineFields({
tag: field({
mode: 'multi',
decode: pipe(mapItems(trim), unique),
}),
});
const codec = createURLSearchParamsCodec(searchFields);
codec.encode({ tag: ['react', 'router'] }).toString();
// tag=react&tag=router如果 multi field 有 alias,写回规则仍然一样:使用 canonical key,并在该 field 被 patch 时清理 legacy alias。
#Decode
decode 用于把 URLSearchParams 还原成完整 values。
const values = codec.decode(new URLSearchParams('?q=router&page=2'));直接调用 decode 更适合测试、fixture、服务端逻辑,或需要从已有 URLSearchParams 生成 typed values 的工具代码。