如何为接收Alpha类型对象集合的TypeScript函数正确类型标注?
问题:TypeScript多对象场景下
Alpha类型的foo与bar关联校验失效 我们定义了Alpha类型,其中bar字段的类型由foo字段的具体字符串值决定。单个对象通过echo函数可以正确校验这种关联,但接收多个键值对对象的bravo函数无法校验每个对象的foo与bar的对应关系,预期的类型错误无法触发。
原代码示例
// We have this Alpha type and echo function... type NoInfer<T> = [T][T extends unknown ? 0 : never] interface Alpha<Foo extends string> { foo: Foo bar: `Depends on ${NoInfer<Foo>}` } declare const echo: <T extends string>(x: Alpha<T>) => void echo({ foo: 'beta', bar: 'Depends on beta'}) // @ts-expect-error Trailing 2 is wrong echo({ foo: 'beta', bar: 'Depends on beta 2'}) // Now we want a function (bravo) that takes a keyed index of Alphas... declare const bravo: <T extends { [k: string]: Alpha<string> }>(xs: T) => void bravo({ one: { foo: `1`, bar: `Depends on 1` }, // @ts-expect-error 1 !== 1x <-- fails oneX: { foo: `1x`, bar: `Depends on 1` }, two: { foo: `2`, bar: `Depends on 2` }, // @ts-expect-error 2 !== 2x <-- fails twoX: { foo: `2x`, bar: `Depends on 2` }, }) // how could this work?
解决方案
问题出在原bravo函数的泛型约束上:T extends { [k: string]: Alpha<string> }会让所有Alpha实例的Foo类型被统一推断为string,而非每个对象的具体字符串字面量,导致bar的类型被放宽为Depends on string,无法进行精确校验。
我们需要让bravo的泛型能够捕获每个对象的foo具体值,从而约束对应的bar类型。以下是两种可行方案:
方案1:直接用映射类型约束每个属性
type NoInfer<T> = [T][T extends unknown ? 0 : never] interface Alpha<Foo extends string> { foo: Foo bar: `Depends on ${NoInfer<Foo>}` } declare const echo: <T extends string>(x: Alpha<T>) => void // 修改bravo的泛型定义 declare const bravo: <T extends Record<string, { foo: string }>>( xs: { [K in keyof T]: Alpha<T[K]["foo"]> } ) => void bravo({ one: { foo: `1`, bar: `Depends on 1` }, // @ts-expect-error 正确触发:bar值不匹配foo的"1x" oneX: { foo: `1x`, bar: `Depends on 1` }, two: { foo: `2`, bar: `Depends on 2` }, // @ts-expect-error 正确触发:bar值不匹配foo的"2x" twoX: { foo: `2x`, bar: `Depends on 2` }, })
方案2:定义专用的映射类型(复用性更强)
如果需要多次使用这种多Alpha对象的类型,可以单独定义映射类型:
type NoInfer<T> = [T][T extends unknown ? 0 : never] interface Alpha<Foo extends string> { foo: Foo bar: `Depends on ${NoInfer<Foo>}` } // 定义Alpha对象的映射类型 type AlphaRecord<T extends Record<string, string>> = { [K in keyof T]: Alpha<T[K]> } declare const echo: <T extends string>(x: Alpha<T>) => void declare const bravo: <T extends Record<string, string>>(xs: AlphaRecord<T>) => void bravo({ one: { foo: `1`, bar: `Depends on 1` }, // @ts-expect-error 正确触发 oneX: { foo: `1x`, bar: `Depends on 1` }, two: { foo: `2`, bar: `Depends on 2` }, // @ts-expect-error 正确触发 twoX: { foo: `2x`, bar: `Depends on 2` }, })
原理说明
修改后的泛型定义会为每个键值对单独推断foo的字符串字面量类型,再通过Alpha<T[K]["foo"]>约束对应的bar必须匹配Depends on ${具体foo值}的模板字符串类型,从而实现每个对象的foo与bar的关联校验。
内容的提问来源于stack exchange,提问作者Jason Kuhrt
相关产品推荐
相关产品推荐

