类型工具
DataOf、TemplateProps、LoadedRegistry、TemplateDef 的类型机制与模块增强原理
框架提供的类型工具全部基于 TypeScript 的类型推导和模块增强,让模板路由和数据类型在调用侧自动补全和校验。
DataOf
type <> = extends <infer > ? : never从模板定义类型里提取数据类型 D,核心价值是类型传递:模板定义时标注的泛型 D 可以一路流动到渲染调用侧。
基本用法
import { , type , type } from '@karinjs/template-react'
interface CardData {
: string
: number
}
const = ({
: ({ }: <CardData>) => <>{.}</>
})
// 提取数据类型
type ExtractedData = <typeof >ExtractedData 自动推导为 CardData,无需手写类型注解。
配合注册表使用
最典型的应用是渲染函数封装,配合 LoadedRegistry 实现逐路由类型推导:
// 调用时自动推导
renderImage('hello/card', { : 'Hello', : [{ : '分数', : '95' }] })K 是路由字面量(如 'hello/card'),Registry[K] 取到该路由的模板定义,DataOf<Registry[K]> 提取其数据类型。
工作原理
DataOf 利用 TypeScript 的条件类型 + infer 关键字:
T extends TemplateDef<infer D>判断T是否符合TemplateDef<D>的结构。- 若符合,
infer D捕获泛型参数D,返回D。 - 若不符合,返回
never。
TemplateDef 内部有隐藏字段 __data?: D(运行时从不使用),唯一作用是把 D 保留在类型上供 infer 提取:
interface <> {
?: string
?: string
: <any>
?: (: unknown) => is
?: // 类型占位符,运行时不存在
}TemplateProps
interface <> {
/** 当前模板使用的数据,类型由 defineTemplate 的泛型决定。 */
:
/** ktr 注入的运行时上下文。 */
: RenderContext
}每个模板组件的 props 类型,泛型 D 是数据类型。
完整示例
import { , type } from '@karinjs/template-react'
/** 排行榜模板的数据结构。 */
export interface RankData {
: string
: <{ : string; : number; : number }>
}
const = ({ , }: <RankData>) => {
const = .?. === 'dark'
return (
< ={`w-[640px] p-6 ${ ? 'dark' : 'light'}`}>
< ="text-2xl font-bold mb-4">{.}</>
< ="space-y-2">
{..(() => (
< ={.} ="flex items-center gap-3">
< ="text-lg font-mono">#{.}</>
< ="flex-1">{.}</>
< ="font-bold text-accent">{.}</>
</>
))}
</>
</>
)
}
export default ({
: '排行榜',
: '展示用户排名和分数',
:
})标注 TemplateProps<RankData> 后,组件内 data 的类型自动推导为 RankData,ctx 类型为 RenderContext。
类型推导链路
RankData 接口定义
↓
TemplateProps<RankData> 标注组件 props
↓
defineTemplate 自动推导泛型 <RankData>
↓
TemplateDef<RankData> 保留 __data?: RankData
↓
DataOf<TemplateDef<RankData>> 提取 RankData
↓
渲染函数 data 参数类型为 RankDataLoadedRegistry
type = keyof ProjectRegistry extends never ? : ProjectRegistry约定加载(loadTemplateRegistry)返回的注册表类型,是一个条件类型别名:模块增强生效时 ProjectRegistry 携带逐路由精确类型,LoadedRegistry 解析为它;未增强时 keyof ProjectRegistry extends never 成立,退化为 AnyRegistry(Record<string, TemplateDef<any>>),路由与 data 类型放宽。
模块增强机制
ktr sync 扫描 ktr/template/ 后,生成 .ktr/registry-types.d.ts:
// 此文件由 @karinjs/template-react 自动生成,请不要手动修改。
// 按约定维护 ktr/template/ 下的组件、mock 与 JSON 数据,运行 ktr sync/dev/build 会自动刷新这里。
// 下游源码无需 import .ktr:ktr sync 会把逐路由精确类型注入 @karinjs/template-react/registry-types。
export {}
declare module '@karinjs/template-react/registry-types' {
interface ProjectRegistry {
'hello/card': typeof import('../ktr/template/hello/card/index').default
'hello/list': typeof import('../ktr/template/hello/list/index').default
'user/profile': typeof import('../ktr/template/user/profile/index').default
}
}TypeScript 的模块增强会把这些路由合并到 ProjectRegistry 接口上,LoadedRegistry 随之解析为逐路由精确类型,效果等价于:
interface ProjectRegistry {
'hello/card': TemplateDef<CardData>
'hello/list': TemplateDef<HelloListData>
'user/profile': TemplateDef<ProfileData>
}类型增强效果
增强后,渲染函数的路由参数和数据参数自动联动:
// 路由字面量自动补全,data 类型由路由推导
await renderTemplate('hello/card', { : 'Hello', : [] })
// 类型错误会在编译期报红
// @ts-expect-error
await ('hello/card', { : 123 })类型失效排查
若路由补全突然「不灵了」,按以下步骤排查:
- 检查
.ktr/registry-types.d.ts是否存在:不存在时跑pnpm ktr sync生成。 - 检查 tsconfig.json 的 include:确保包含
.ktr/**/*.d.ts。 - 重启 TS Server:VSCode 中按
Ctrl+Shift+P→TypeScript: Restart TS Server。 - 检查模板导出:确保模板组件通过
export default defineTemplate(...)默认导出。增强通过typeof import('...').default提取类型,内联/匿名数据类型也能精确推导,不要求数据接口具名导出。
未增强时的类型退化
增强是否生效是编译期问题,只取决于 .ktr/registry-types.d.ts 是否被下游 tsconfig 的 include 覆盖,与生产部署无关。未被 TS 工程包含时 keyof ProjectRegistry extends never 成立,LoadedRegistry 退化为 AnyRegistry(Record<string, TemplateDef<any>>),失去逐路由类型推导,运行时行为不受影响。
TemplateDef
interface <> {
/** 面板侧边栏展示名称。 */
?: string
/** 面板展示的模板描述。 */
?: string
/** 实际渲染截图的组件。 */
: <<>>
/** 运行时数据校验,返回 false 时 SSR 直接报错。 */
?: (: unknown) => is
/** 类型占位符,运行时从不读取,仅供 DataOf 提取类型。 */
?:
}模板定义的完整结构,defineTemplate 的参数和返回值都是这个类型(去掉 __data 字段)。
component 字段
要求是 React.ComponentType<TemplateProps<D>>,即接收 { data: D, ctx: RenderContext } 的组件。
支持函数组件和类组件,也支持异步组件(React 18+ 的 Suspense):
import { , type } from '@karinjs/template-react'
interface AsyncData {
: string
}
// 异步组件
const = async ({ }: <AsyncData>) => {
const = await (`/api/users/${.}`).(() => .())
return <>{.name}</>
}
export default ({
:
})异步组件在 React 18+ 的 SSR 中可用,但需要在组件外包裹 <Suspense fallback={...}>。框架暂不自动包裹,请在组件内自行处理或使用同步组件。
validate 字段
类型保护函数,签名为 (data: unknown) => data is D,返回 true 时断言 data 符合 D 类型。
适用场景:动态路由、用户输入数据、跨插件传递数据等不可信来源。
import { , type } from '@karinjs/template-react'
interface SafeData {
: number
: string
}
function (: unknown): is SafeData {
return (
typeof === 'object' && !== null && typeof ( as SafeData). === 'number' && typeof ( as SafeData). === 'string'
)
}
const = ({ }: <SafeData>) => (
<>
ID: {.}, Name: {.}
</>
)
export default ({
: ,
:
})校验失败时渲染器返回 { success: false, error: 'Template data validation failed' },不会执行 React 渲染。
__data 字段
类型占位符,运行时从不存在,唯一作用是把泛型 D 保留在类型上供 DataOf 提取。
日常不需要关心这个字段,defineTemplate 会自动处理。手写 TemplateDef 类型时也不需要显式赋值:
import type { } from '@karinjs/template-react'
interface MyData {
: string
}
// 正确:不需要写 __data
const : <MyData> = {
: () => <>Hello</>
}
// 不推荐:__data 只是类型占位符,运行时从不读取,手动赋值没有意义
const : <MyData> = {
: () => <>Hello</>,
: { : 'bar' }
}类型推导最佳实践
1. 数据接口无需具名导出
模块增强通过 typeof import('../path/to/template').default 从默认导出提取完整的 TemplateDef<D>,数据类型跟随默认导出走,不要求接口具名导出。具名导出接口方便复用,内联匿名类型同样能被精确推导:
import { , type } from '@karinjs/template-react'
// ✅ 可以:具名导出接口,方便在其他文件复用
export interface CardData {
: string
}
const = ({ }: <CardData>) => <>{.}</>
export default ({ : })import { , type } from '@karinjs/template-react'
// ✅ 也可以:内联匿名类型,typeof import('...').default 仍能精确提取
const = ({ }: <{ : string }>) => <>{.}</>
export default ({ : })2. 组件 props 优先标注
让 defineTemplate 自动推导泛型,而不是手动指定:
import { , type } from '@karinjs/template-react'
interface CardData {
: string
}
// ✅ 推荐:组件标注 TemplateProps<CardData>,defineTemplate 自动推导
const = ({ }: <CardData>) => <>{.}</>
export default ({ : })
// ❌ 不推荐:手动指定泛型,容易和组件 props 不一致
// const Card2 = ({ data }: any) => <div>{data.title}</div>
// export default defineTemplate<CardData>({ component: Card2 })3. 渲染函数使用泛型约束
封装渲染函数时,用 K extends keyof Registry & string 约束路由参数:
import type { , , , RenderResult } from '@karinjs/template-react'
type =
// ✅ 正确:泛型约束 + DataOf 提取
async function < extends keyof & string>(
: ,
: <[]>,
?:
): <RenderResult> {
// 实现...
return { : true, : '/path/to/output.html' }
}
// ❌ 错误:丢失类型推导
async function (: string, : any): <RenderResult> {
// 无法推导 data 类型
return { : true, : '/path/to/output.html' }
}4. 避免类型断言
信任 TypeScript 的推导,不要强制类型断言:
import { } from '@karinjs/template-react'
const = (import.meta.)
// ✅ 正确:类型自动推导
await ('hello/card', { : 'Hello', : [] })
// ❌ 错误:强制断言丢失类型检查
await ('hello/card', { : 123 } as any)类型错误是编译期保护,不要用 as any 绕过。