Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make union function types callable. #57400

Closed
6 tasks done
ghost opened this issue Feb 13, 2024 · 7 comments
Closed
6 tasks done

Make union function types callable. #57400

ghost opened this issue Feb 13, 2024 · 7 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@ghost
Copy link

ghost commented Feb 13, 2024

πŸ” Search Terms

Each member of the union type has signatures, but none of those signatures are compatible with each other, expression not callable, incompatible signatures,

βœ… Viability Checklist

⭐ Suggestion

Currently typescript does not allow executing generic functions with incompatible union type. However such behavior is not contradicting any type-safety mechanisms. As you will see from the example below there is no way to call such function without "type assertion" and there is no need for asserting the type on this phase of the programming. It would unwrap it's type based on the higher level code that will use this function and would specify the concrete needs for the exact argument types.

πŸ“ƒ Motivating Example

class Test {
    execute(a: number){
        return {
            a: 12,
            b: "hey"
        }
    }
    run(a: string){
        return{
            c: true
        }
    }
}

// As you can see the arguments 
// that will be passed to the function 
// are derived from the parameters of the function 
// thus there is no type violation

function D<T extends "execute" | "run">(arg: Test[T], value: Parameters<Test[T]>){
    arg(value) // -> error here 
                      //"Argument of type '[a: number] | [a: string]' is not assignable to 
                      // parameter of type 'never'.
                      // Type '[a: number]' is not assignable to type 'never'."
}

D<"execute">() // -> here we know already the type 
// and is narrowed enough for the argument to be the one of the same type 
// that the specific function accepts
// there is no way to mess the types and type safety is preserved

This shall be allowed in my opinion as it does not put at risk of type mismatch

πŸ’» Use Cases

  1. What do you want to use this for?
    It would allow developers to abstract away function calls and build on top of other libraries that have not taken into account the need for abstracting away common logic and fields
  2. What workarounds are you using in the meantime?
    There is no workaround currently. It is possible to narrow the type,however such a solution is not the prefered way and it polutes the codebase.
@MartinJohns
Copy link
Contributor

This shall be allowed in my opinion as it does not put at risk of type mismatch

Sure it does. It's perfectly legal to call your function this way:

D<"execute" | "run">((a: number) => ({ a: 0, b: "string" }), ["abc"])

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 13, 2024
@RyanCavanaugh
Copy link
Member

Neither here nor there, but the function signature is also wrong. A closer fit would be

function D<T extends "execute" | "run">(arg: Test[T], value: Parameters<Test[T]>[0])

@fatcerberus
Copy link

Linking to #57372 (comment) as it's relevant here too

@ghost
Copy link
Author

ghost commented Feb 14, 2024

Thank you for the quick answer!
I tried to simplify my concrete case and created a total nonsense. So let me give you a more precise version of what limitations I encounter.

// d.ts --------------- START 
// STUCK WITH THIS, NO WAY TO CHANGE, PART OF A LIBRARY
// SIMPLIFIED 

interface Model1Functions {
    find<T extends "string" | "boolean">(args: T): string;
    create<T extends "string">(args: T): string
}

interface Model2Functions {
    find<T extends "number">(args: T): number;
    create<T extends "boolean">(args: T): string;
}

declare class ModelGetter {
    get model1(): Model1Functions
    get model2(): Model2Functions
}

// d.ts --------------- END

// MY CODE
function functionWrapper<
    M extends "model1" | "model2",
    F extends "find" | "create", 
>(fn: ModelGetter[M][F], value: Parameters<ModelGetter[M][F]>[0]){
    fn(value) // -----> Error here
    // This expression is not callable.
    // Each member of the union type 
    // '(<T extends "string" | "boolean">(args: T) => string) | 
    // (<T extends "number">(args: T) => number) | 
    // (<T extends "string">(args: T) => string) | 
    // (<T extends "boolean">(args: T) => string)' 
    // has signatures, but none of those signatures are compatible with each other.
    // (TS 2349)
}

What I would like to know is whether there is a way to create functionWrapper and call inside of it one of the functions which all have overlapping names and one argument only. If we pass to the generic function "functionWrapper" M (the Model name) and F(the function inside the model) as far as I can see type is narrowed enough to preserve all type-safety. It might be the case that the library which generated those declared types isn't perfect (I am not arguing that), because it does not use any inheritance, which might suit the case better, however this is the reality for now.

@RyanCavanaugh
Copy link
Member

Not the way you've written it; the call

declare const m: Model1Functions;
functionWrapper<"model1" | "model2", "find" | "create">(m.create, "boolean");

is legal but unsound, thus the implementation must error.

Further guidance sort of hinges on why you're not just writing

function functionWrapper<Arg>(fn: (arg: Arg) => unknown, value: Arg) {
    fn(value) // OK
}

Likely your use case boils down to #27808, though

@fatcerberus
Copy link

I get the sense that #30581 might be relevant

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Feb 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants