Sanity Typegen 如何正确推断 GROQ 动态语言索引的返回类型?
可以通过改写查询,让 Typegen 推断出 string | null,省去在每个组件中添加类型断言的麻烦。对于 pl、en 这类已知语言字段,建议用 select() 明确写出各语言对应的属性。
运行时 heading[$lang] 返回字符串,并不保证静态类型生成也能识别这种访问方式。具体行为仍要以项目使用的 Sanity 版本和生成结果为准。
为什么动态索引会推断成对象?
Typegen 根据提取后的 schema 和查询表达式生成类型。它不会执行实际的数据请求,也不会根据某次 fetch() 传入的 { lang: "pl" } 推断返回类型。Sanity TypeGen 文档
GROQ 的方括号既可以表示属性访问,也可以表示过滤。官方文档要求对象属性访问中的属性名使用字符串字面量,以免与过滤表达式产生歧义。因此,不能直接套用 JavaScript 中 object[key] 的规则,认定静态分析会把 heading[$lang] 识别为动态属性访问。GROQ 运算符文档
即使当前运行环境能返回预期字符串,也需要分别核对运行时结果和 Typegen 的推断结果。
使用 select() 明确返回字段
先确认字段名:题目中的 schema 定义的是 title,查询用的却是 heading。下面假设实际字段名为 heading,内部有 pl、en 两个字符串字段。如果实际字段名是 title,请将属性访问相应改成 title.pl、title.en。
const HERO_QUERY = defineQuery(`
*[_type == "home"][0] {
"heading": select(
$lang == "pl" => heading.pl,
$lang == "en" => heading.en,
null
)
}
`);
语言仍由 $lang 选择,每个分支访问的属性则已明确,Typegen 可以根据 schema 分析分支的返回值。select() 返回第一个条件成立的分支,最后的 null 用于处理没有匹配到的语言。GROQ 函数文档
如果两个语言字段都是可选字符串,预期结果类型为:
export type HERO_QUERY_RESULT = {
heading: string | null;
} | null;
外层也要保留 | null,因为 [0] 可能找不到任何 home 文档。文档存在但缺少对应翻译时,heading 为 null。
重新生成类型并检查调用处
在 Sanity 项目中,按现有配置重新提取 schema 并生成类型:
npx sanity schema extract
npx sanity typegen generate
这两个命令分别更新供 Typegen 使用的 schema 和生成的 TypeScript 类型。查询文件需要位于 Typegen 配置的扫描路径内。类型生成流程
如果项目已配置好查询与客户端的自动类型关联,调用时继续传入参数即可:
const hero = await client.fetch(HERO_QUERY, {
lang: "pl",
});
const heading = hero?.heading ?? "";
修改后如果仍出现旧的对象类型,先检查查询是否被扫描、生成文件是否更新,以及 TypeScript 是否加载了正确的生成文件。不要直接修改 types.ts,手工修改的内容会在下次生成时被覆盖。
默认语言回退与手动覆盖
如果希望缺少翻译时回退到英语,可以把投影改为:
"heading": coalesce(
select(
$lang == "pl" => heading.pl,
$lang == "en" => heading.en,
null
),
heading.en
)
coalesce() 返回第一个非 null 的值。如果对应翻译和英语翻译都不存在,结果仍为 null;空字符串不会触发回退。未知语言也会回退到英语,而前一个示例会返回 null。coalesce() 文档
如果必须保留原来的动态查询,可以在统一的数据访问函数中校验结果,再向组件返回 { heading: string | null } | null。确实需要类型断言时,也应集中放在这一层。断言只改变 TypeScript 的检查方式,不会验证真实数据;逐行使用 @ts-expect-error 则会把所在行的其他类型错误也一起屏蔽。
备注:内容仅供参考。
