API 参考

类型工具

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 >
type ExtractedData = CardData

ExtractedData 自动推导为 CardData,无需手写类型注解。

配合注册表使用

最典型的应用是渲染函数封装,配合 LoadedRegistry 实现逐路由类型推导:

// 调用时自动推导
renderImage('hello/card', { : 'Hello', : [{ : '分数', : '95' }] })
const renderImage: <"hello/card">(templatePath: "hello/card", data: {
    title: string;
    items: {
        label: string;
        value: string;
    }[];
}) => Promise<void>

K 是路由字面量(如 'hello/card'),Registry[K] 取到该路由的模板定义,DataOf<Registry[K]> 提取其数据类型。

工作原理

DataOf 利用 TypeScript 的条件类型 + infer 关键字:

  1. T extends TemplateDef<infer D> 判断 T 是否符合 TemplateDef<D> 的结构。
  2. 若符合,infer D 捕获泛型参数 D,返回 D
  3. 若不符合,返回 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 的类型自动推导为 RankDatactx 类型为 RenderContext

类型推导链路

RankData 接口定义

TemplateProps<RankData> 标注组件 props

defineTemplate 自动推导泛型 <RankData>

TemplateDef<RankData> 保留 __data?: RankData

DataOf<TemplateDef<RankData>> 提取 RankData

渲染函数 data 参数类型为 RankData

LoadedRegistry

type  = keyof ProjectRegistry extends never ?  : ProjectRegistry

约定加载(loadTemplateRegistry)返回的注册表类型,是一个条件类型别名:模块增强生效时 ProjectRegistry 携带逐路由精确类型,LoadedRegistry 解析为它;未增强时 keyof ProjectRegistry extends never 成立,退化为 AnyRegistryRecord<string, TemplateDef<any>>),路由与 data 类型放宽。

模块增强机制

ktr sync 扫描 ktr/template/ 后,生成 .ktr/registry-types.d.ts

.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', : [] })
const renderTemplate: <"hello/card">(templatePath: "hello/card", data: {
    title: string;
    items: {
        label: string;
        value: string;
    }[];
}, ctx?: RenderContextInput) => Promise<RenderResult>
// 类型错误会在编译期报红 // @ts-expect-error await ('hello/card', { : 123 })

类型失效排查

若路由补全突然「不灵了」,按以下步骤排查:

  1. 检查 .ktr/registry-types.d.ts 是否存在:不存在时跑 pnpm ktr sync 生成。
  2. 检查 tsconfig.json 的 include:确保包含 .ktr/**/*.d.ts
  3. 重启 TS Server:VSCode 中按 Ctrl+Shift+PTypeScript: Restart TS Server
  4. 检查模板导出:确保模板组件通过 export default defineTemplate(...) 默认导出。增强通过 typeof import('...').default 提取类型,内联/匿名数据类型也能精确推导,不要求数据接口具名导出。

未增强时的类型退化

增强是否生效是编译期问题,只取决于 .ktr/registry-types.d.ts 是否被下游 tsconfig 的 include 覆盖,与生产部署无关。未被 TS 工程包含时 keyof ProjectRegistry extends never 成立,LoadedRegistry 退化为 AnyRegistryRecord<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 绕过。

On this page