如何在TypeScript中为基于路径解析的watchData回调正确类型标注
问题:为watchData函数实现正确的类型标注,让回调参数自动推断类型
背景与代码基础
首先定义包含Widget和Sprocket的数据库类型及实例:
type Widget = { foo: unknown, bar: unknown } type Sprocket = { baz: unknown, qux: unknown } type Widgets = Record<string, Widget> type Sprockets = Record<string, Sprocket> const database: {widgets: Widgets, sprockets: Sprockets} = { widgets: { 123: { foo: 'something', bar: 'something' }, 234: { foo: 'something', bar: 'something' } }, sprockets: { 345: { baz: 'something', qux: 'something' }, 456: { baz: 'something', qux: 'something' } } }
期望实现watchData函数,通过传入路径订阅数据,回调的data参数能根据路径自动推断类型:
watchData('widgets/123', data => { // 编译器应自动推断data为Widget类型 console.log(data) })
已定义Data类型,能正确解析路径对应的类型:
type Data<Path> = Path extends `${infer Table}/${infer _Key}` ? (Table extends 'widgets' ? Widget : (Table extends 'sprockets' ? Sprocket : undefined)) : Path extends `${infer Table}` ? (Table extends 'widgets' ? Widgets : Table extends 'sprockets' ? Sprockets : undefined) : undefined // 测试验证:类型推断正确 type TestSomeWidget = Data<'widgets/123'> // Widget type TestSomeSprocket = Data<'sprockets/456'> // Sprocket type TestAllWidgets = Data<'widgets'> // Widgets type TestAllSprockets = Data<'sprockets'> // Sprockets
尝试的基础实现报错,因为TypeScript无法关联回调参数类型与运行时逻辑:
const watchData = <Path extends string>(path: Path, cb: (val: Data<Path>) => void): unknown => { const [table, key] = path.split('/') if (table === 'widgets' || table === 'sprockets') { const tableData = database[table] if (key) return cb(tableData[key]) // 类型不匹配报错 return cb(tableData) // 类型不匹配报错 } return cb(undefined) }
解决方案:使用函数重载实现精确类型推断
函数重载能直接为不同路径模式定义对应的回调参数类型,外部调用时自动推断,内部逻辑无需复杂泛型关联:
// 重载1:订阅整个widgets表,回调参数为Widgets类型 function watchData(table: 'widgets', cb: (val: Widgets) => void): unknown; // 重载2:订阅整个sprockets表,回调参数为Sprockets类型 function watchData(table: 'sprockets', cb: (val: Sprockets) => void): unknown; // 重载3:订阅单个widget条目,回调参数为Widget类型 function watchData(path: `${'widgets'}/${string}`, cb: (val: Widget) => void): unknown; // 重载4:订阅单个sprocket条目,回调参数为Sprocket类型 function watchData(path: `${'sprockets'}/${string}`, cb: (val: Sprocket) => void): unknown; // 重载5:处理无效路径,回调参数为undefined function watchData(path: string, cb: (val: undefined) => void): unknown; // 函数实现(内部可安全使用类型兼容的逻辑) function watchData(path: string, cb: (val: any) => void): unknown { const [table, key] = path.split('/'); if (table === 'widgets') { const tableData = database.widgets; return key ? cb(tableData[key]) : cb(tableData); } if (table === 'sprockets') { const tableData = database.sprockets; return key ? cb(tableData[key]) : cb(tableData); } return cb(undefined); }
使用验证
调用时编译器会自动推断回调参数类型:
// data 类型为 Widget watchData('widgets/123', data => { console.log(data.foo); // 可正常访问Widget属性 }); // data 类型为 Widgets watchData('widgets', data => { console.log(data['123'].bar); // 可正常访问嵌套属性 }); // data 类型为 Sprocket watchData('sprockets/456', data => { console.log(data.baz); }); // data 类型为 undefined watchData('invalid/path', data => { console.log(data); });
原实现报错原因
TypeScript无法将运行时的table/key值与泛型Path做关联推导,比如当Path是'widgets/123'时,编译器无法确认table就是'widgets'、key就是'123',因此无法认定tableData[key]的类型等于Data<Path>,导致类型不匹配报错。函数重载通过预先定义路径与类型的对应关系,避开了这种复杂的泛型关联问题。
内容的提问来源于stack exchange,提问作者akivajgordon
相关产品推荐
相关产品推荐

