Search code examples
typescripttypescript-generics

TypeScript Type Get Return Type of Constrained Function Type Generics


Basically, I am trying to find a way to use function types as generic types to find the return type of the function with the constraint that I choose. However, I'm not sure if it's possible because I don't understand how generic function parameters work in conditional clauses. Here is what I'm trying currently:

type FooFormatter = <S extends string>(input: S) => `foo-${S}`;

type StringFormatter = <S extends string>(input: S, ...args: never) => string;

type ComputeFormatter<Input extends string, T extends StringFormatter> =
    T extends (input: Input, ...args: never) => `${infer $Value}`
        ? $Value
        : never;

type foo = ComputeFormatter<"hey", FooFormatter>; // this is `foo-${string}` but I want "foo-hey"

TypeScript Playground Link

In ComputeFormatter I'm trying to check if I can somehow constrain the generic in T by overwriting the function's first parameter to the Input type. Thanks for any help!


Solution

  • Short answer, there is an PR merged for TS4.7 for this feature. Of particular relevance to your question...

    type Box<T> = ReturnType<typeof makeBox<T>>;  // { value: T }
    

    This is super close to a solution of #37181. Will it will allow us to do...

    return { value }; };
    
    // Can we do it or something similar? Currently doesn't compile :(
    type MakeBox<T> = ReturnType<typeof makeBox<T>>
    
    // As it now allows us to do (no generics though)
    const stringMakeBox = makeBox<string>; type MakeBox = ReturnType<typeof stringMakeBox>
    
    // And even more relevant to support generics if we can now do: type
    MakeBox = ReturnType<typeof makeBox<string>> 
    

    Yes, you can indeed use that pattern to capture a generic return type of a generic function. This is something that previously wasn't possible. I'll update the PR description to include an example.

    Comment here

    I can't find anything on whether this works on type aliases for functions, or just works on runtime functions only (inferred with typeof), but you can possibly just use the actual runtime implementations and use typeof as needed

    Here is this working on the nightly build, note that you'll still need a runtime implementation, whether or not it actually implements anything is up to you, it could be some mock function that doesn't return anything

    type FooFormatter = <S extends string>(input: S) => `foo-${S}`;
    
    const FooFormatterImplementation: FooFormatter = {} as any; //mock implementation
    
    type StringFormatter = <S extends string>(input: S, ...args: never) => string;
    
    type foo = ReturnType<typeof FooFormatterImplementation<"hey">>
    //  ^? `type foo = "foo-hey"`
    

    My two-cents and more (More Post Script stuff)

    I highly recommend investigating some of the types they have over at type-fest, if you haven't already. Especially

    Reading through the library, it looks like you've got a good sense of types, and I think you may actually be limited by the TS version of Deno.

    There are a bunch of workarounds, but I'm not sure if they're really applicable in your usecase.

    Edit: Workaround with declaration merging and interfaces

    This is not dependent upon TS4.7 Hooray!

    type FooFormatter = <S extends string>(input: S) => `foo-${S}`;
    
    type BarFormatter = <S extends string>(input: S) => `bar-${S}`
    
    export interface Formatters {
        FooFormatter: FooFormatter,
        BarFormatter: BarFormatter
    }
    // then anyone wanting to add a custom formatter, has to modify and reexport the interface,
    // this is similiarly done in @react-mui, using module augmentation
    // https://www.typescriptlang.org/docs/handbook/declaration-merging.html
    
    function takeInnerTest<T extends string, FmtKey extends keyof Formatters>(input: T, formatter: FmtKey)
    {
        type formattersDiscriminated = {
            [K in keyof Formatters]: Formatters[K]
        }[FmtKey]
        const __mockFunction: formattersDiscriminated = ((...args: any[]) => undefined) as any
        const __mockReturn = __mockFunction(input)
        type String = ReturnType<typeof __mockFunction>
        type ReturnValue = Extract<typeof __mockReturn, String>
        return null! as ReturnValue
    }
    
    const foo3 = takeInnerTest("hey", "FooFormatter")
    type foo3 = typeof foo3
    //  ^?
    const foo4 = takeInnerTest("hey", "BarFormatter")
    type foo4 = typeof foo4
    //  ^?
    

    TS Playground