TypeScript:如何基于传入函数返回类型缩小processFooBar返回类型并保留命名类型
TypeScript 类型推断问题:动态调整返回类型并保留命名类型
我在 TypeScript 4.9.4 版本中遇到了类型推断问题,需求是让函数processFooBar的返回类型根据传入的内部函数类型自动调整,同时保留有意义的命名推断类型(比如GetFoo<T>而非() => Foo<T>)。
目前我的实现仅能在调用时显式指定类型参数的情况下正确推断类型,但这不符合需求,代码如下:
import { expectAssignable, expectNotType, expectType } from 'tsd'; type Foo<T> = { foo: true, value: T }; type Bar = { foo: false }; type FooBar<T> = Foo<T> | Bar; const fooInstance: Foo<number> = { foo: true, value: 42 }; const barInstance: Bar = { foo: false }; expectType<Foo<number>>( fooInstance ); // pass expectNotType<Foo<number>>( barInstance ); // pass expectNotType<Bar>( fooInstance ); // pass expectType<Bar>( barInstance ); // pass expectAssignable<FooBar<number>>( fooInstance ); // pass expectAssignable<FooBar<number>>( barInstance ); // pass type GetFoo<T> = () => Foo<T>; type GetFooBar<T> = () => FooBar<T>; const getFooInstance = () => fooInstance; const getBarInstance = () => barInstance; const getFooBarInstance = () => fooInstance as FooBar<number>; expectType<GetFoo<number>>( getFooInstance ); // pass expectNotType<GetFoo<number>>( getBarInstance ); // pass expectNotType<GetFoo<number>>( getFooBarInstance ); // pass expectAssignable<GetFooBar<number>>( getFooInstance ); // pass expectAssignable<GetFooBar<number>>( getBarInstance ); // pass expectType<GetFooBar<number>>( getFooBarInstance ); // pass export type GetterFromFooBar<R,T1,T2 extends GetFooBar<T1>> = [T2] extends [GetFoo<T1>] ? GetFoo<R> : GetFooBar<R>; // 如何修改这个函数,在传入GetFoo类型的getter时自动将返回类型缩小为GetFoo, // 且无需在调用时显式指定类型参数? function processFooBar<R,T1,T2 extends GetFooBar<T1>>( getter: T2, mapper: (input: T1) => R ) { return (() => { const fooBar = getter(); return (fooBar.foo ? ({ foo: true, value: mapper(fooBar.value) }) : ({ foo: false }) ); }) as GetterFromFooBar<R,T1,T2>; } expectType<GetFoo<number>>( processFooBar<number, number, GetFoo<number>>(getFooInstance, x => x) ); // pass, 显式指定类型参数 expectNotType<GetFoo<number>>( processFooBar<number, number, GetFooBar<number>>(getBarInstance, x => x) ); // pass, 显式指定类型参数 expectNotType<GetFoo<number>>( processFooBar<number, number, GetFooBar<number>>(getFooBarInstance, x => x) ); // pass, 显式指定类型参数 expectAssignable<GetFooBar<number>>( processFooBar<number, number, GetFoo<number>>(getFooInstance, x => x) ); // pass, 显式指定类型参数 expectAssignable<GetFooBar<number>>( processFooBar<number, number, GetFooBar<number>>(getBarInstance, x => x) ); // pass, 显式指定类型参数 expectType<GetFooBar<number>>( processFooBar<number, number, GetFooBar<number>>(getFooBarInstance, x => x) ); // pass, 显式指定类型参数 expectType<GetFoo<number>>( processFooBar(getFooInstance, x => x) ); // 错误:类型 'GetFoo<unknown>' 无法赋值给 'GetFoo<number>' expectNotType<GetFoo<number>>( processFooBar(getBarInstance, x => x) ); // pass,但预期应为GetFooBar<number>而非GetFooBar<unknown> expectNotType<GetFoo<number>>( processFooBar(getFooBarInstance, x => x) ); // pass,但预期应为GetFooBar<number>而非GetFooBar<unknown> expectAssignable<GetFooBar<number>>( processFooBar(getFooInstance, x => x) ); // 错误:类型 'GetFoo<unknown>' 无法赋值给 'GetFooBar<number>' expectAssignable<GetFooBar<number>>( processFooBar(getBarInstance, x => x) ); // 错误:类型 'GetFooBar<unknown>' 无法赋值给 'GetFooBar<number>' expectType<GetFooBar<number>>( processFooBar(getFooBarInstance, x => x) ); // 错误:类型 'GetFooBar<unknown>' 无法赋值给 'GetFooBar<number>'
我的疑问
- 是否存在无需显式指定类型参数、能实现预期行为且保留命名返回类型的方案?(最好无需外部依赖)
- 这是TypeScript尚未实现的功能吗?
- 还是这属于TypeScript的Bug?
我知道简单场景下可以用函数重载实现,这也是我当前代码的做法,但该方案在其他复杂场景存在局限性,因此尝试寻找更智能的类型推断方案。
最初的简化代码及尝试过的解决方案
由于最初简化了示例,以下方案并不适用于完整场景:
import { expectAssignable, expectNotType, expectType } from 'tsd'; type Foo = { foo: true, value: string }; type Bar = { foo: false }; type FooBar = Foo | Bar; const fooInstance: Foo = { foo: true, value: '' }; const barInstance: Bar = { foo: false }; expectType<Foo>( fooInstance ); // pass expectNotType<Foo>( barInstance ); // pass expectNotType<Bar>( fooInstance ); // pass expectType<Bar>( barInstance ); // pass expectAssignable<FooBar>( fooInstance ); // pass expectAssignable<FooBar>( barInstance ); // pass type GetFoo = () => Foo; type GetBar = () => Bar; type GetFooBar = () => FooBar; const getFooInstance = () => fooInstance; const getBarInstance = () => barInstance; const getFooBarInstance = () => fooInstance as Foo | Bar; expectType<GetFoo>( getFooInstance ); // pass expectNotType<GetFoo>( getBarInstance ); // pass expectNotType<GetFoo>( getFooBarInstance ); // pass expectNotType<GetBar>( getFooInstance ); // pass expectType<GetBar>( getBarInstance ); // pass expectNotType<GetBar>( getFooBarInstance ); // pass expectAssignable<GetFooBar>( getFooInstance ); // pass expectAssignable<GetFooBar>( getBarInstance ); // pass expectType<GetFooBar>( getFooBarInstance ); // pass export type GetterFromFooBar<T extends FooBar> = T extends Foo ? GetFoo : GetFooBar; // 如何修改这个函数,在传入GetFoo类型的getter时自动将返回类型缩小为GetFoo? // 内联GetterFromFooBar只会让推断类型变为GetFooBar function processFooBar(getter: GetFooBar) : GetterFromFooBar<ReturnType<typeof getter>> { return getter; } expectType<GetFoo>( processFooBar(getFooInstance) ); // 错误:类型 'GetFoo | GetFooBar' 无法赋值给 'GetFoo' expectNotType<GetFoo>( processFooBar(getBarInstance) ); // pass,但原因不正确 expectNotType<GetFoo>( processFooBar(getFooBarInstance) ); // pass,但原因不正确 expectNotType<GetBar>( processFooBar(getFooInstance) ); // pass,但原因不正确 expectType<GetBar>( processFooBar(getBarInstance) ); // 错误:类型 'GetFoo | GetFooBar' 无法赋值给 'GetBar' expectNotType<GetBar>( processFooBar(getFooBarInstance) ); // pass,但原因不正确 expectAssignable<GetFooBar>( processFooBar(getFooInstance) ); // pass,但原因不正确 expectAssignable<GetFooBar>( processFooBar(getBarInstance) ); // pass,但原因不正确 expectType<GetFooBar>( processFooBar(getFooBarInstance) ); // 错误:参数类型GetFooBar与实参类型GetFoo | GetFooBar不匹配
@ghybs 和 @jcalz 提供了不保留命名返回类型的解决方案,且无法修改为派生返回类型:
function processFooBar<T extends FooBar>(getter: () => T): () => T { return getter; }
@jcalz 还提供了一个可行方案,但仅适用于简化后的场景,无法适配完整问题:
export type GetterFromFooBar<T extends FooBar> = [T] extends [Foo] ? GetFoo : [T] extends [Bar] ? GetBar : GetFooBar; function processFooBar<T extends FooBar>(getter: () => T) { return getter as GetterFromFooBar<T>; }
内容的提问来源于stack exchange,提问作者Killy.MXI
相关产品推荐
相关产品推荐

