Skip to content

Invoke callable types on type level #54039

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

Closed
5 tasks done
aspirisen opened this issue Apr 26, 2023 · 2 comments
Closed
5 tasks done

Invoke callable types on type level #54039

aspirisen opened this issue Apr 26, 2023 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@aspirisen
Copy link

aspirisen commented Apr 26, 2023

Suggestion

πŸ” Search Terms

Call function type
Invoke function type
Function return type
Generic function return type
Simplify extends conditions

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Allow callable function types invoking on type level. The logic behind it is the same as we have for usual function calling

interface One {
   (): string
}

type Result = One() // Result is string

If function type has arguments we must pass them as well, but use types as argument

interface Data {
   name: string
}

type One = (value: number, data: Data) => boolean
type Result = One(number, Data) // Result is boolean

The same logic with generics

interface Data {
   name: string
}

type One = <T>(value: number, data: T) => T

type Result = One<Data>(number, Data) // Result is Data
type Result = One(number, Data) // Result is Data because of type inference

type Result2 = One(number, boolean) // Result is boolean

However, there is a problem when the type itself has a generic

type One<V> = <T>(value: V, data: T) => T

// Implicitly merge them in one row?
type Result = One<number, Data>(number, Data)

// Think about some other syntax ?
type Result = One<number><Data>(number, Data)

// Surround by brackets?
type Result = (One<number>)<Data>(number, Data)

πŸ“ƒ Motivating Example

Typescript has callable types, i.e.

interface One{
   (value: string): string
}


type Two = () => string

and when we need to to get the return type we can use ReturnType type to inference the type

interface One {
   (value: string): string
}


type Result = ReturnType<One>

But there are problems when you have overloading

interface One {
   (value: string): string
   (value: number): number
}


type Result = ReturnType<One>

The things becomes even more complex if you have generics

interface One {
   <T>(value: T): T
}


type Result = ReturnType<One>

The Result will be just unknown

πŸ’» Use Cases

We will be able to create a type of what function is returning:

declare function createData(condition: 'a'): { a: number; b: number }
declare function createData(condition: 'b'): { c: number; d: number }

type CreateData = typeof createData
type DataA = CreateData('a') // DataA is { a: number; b: number }
type DataB = CreateData('b') // DataB is { c: number; d: number }

We will be able to deal with generic functions

declare function factory<T>(data: T): { getValue(): T; setValue(value: T): void }

type Factory = typeof factory
type MyFactory1 = Factory({ name: string }) // MyFactory1 is { getValue(): { name: string }; setValue(value: { name: string }): void }
type MyFactory2 = Factory({ email: string }) // MyFactory2 is { getValue(): { email: string }; setValue(value: { email: string }): void }

We can simplify extends conditions. For example we have a type with a lot of ? conditions. If there are a lot of conditions it is becomes harder to read them.

interface Data {
   name: string
}


type SomeCondition<T> = T extends number ? 'number' : T extends Data ? 'data' : T extends string ? 'string' : unknown

and since functions can have overloading we can use it as switch case

interface Data {
   name: string
}

interface SomeCondition {
    (value: number): 'number'
    (value: Data): 'data'
    (value: string): 'string'

    // Works as default case
    (...value: any[]): unknown
}

// or

interface SomeCondition {
    <T extends number>(value: T): 'number'
    <T extends Data>(value: T): 'data'
    <T extends string>(value: T): 'string'

    // Works as default case
    (...value: any[]): unknown
}

type A = SomeCondition(number) // A is 'number'
type B = SomeCondition(Data) // B is 'data'
type C = SomeCondition(string) // C is 'string'
type D = SomeCondition(true) // D is unknown

So it works exactly the same as we would just normally call functions

declare const data: Data
declare const logic: SomeCondition

const a = logic(1) // a is 'number'
const b = logic(data) // b is 'data'
const c = logic('foo') // c is 'string'
const d = logic(true) // d is unknown
@fatcerberus
Copy link

Looks like a duplicate of #40179

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Apr 27, 2023
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants