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

Allow using types defined inside function body in the function's signature #61245

Closed
6 tasks done
KurtGokhan opened this issue Feb 21, 2025 Β· 9 comments
Closed
6 tasks done
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@KurtGokhan
Copy link

πŸ” Search Terms

"type alias in function signature", "type alias in function body", "type scope", "duplicate types", "hoist type alias"

βœ… Viability Checklist

⭐ Suggestion

This is a weird request, and I am certain it's not going to be accepted easily. I am basically asking to break lexical scoping for this use case. But I want to show the pain point, so that maybe a better solution can be found.

My suggestion is to hoist type declarations at the top of a function body to outside of the function, so that those type declarations can be used in the function signature.

πŸ“ƒ Motivating Example

Suppose a function like this:

function myFunction<T1, T2>(param1: CalculateParam1Type<T1,T2>, param2: CalculateParam2Type<T1, T2>): CalculateReturnType<T1, T2> {
  type TParam1 = CalculateParam1Type<T1, T2>;
  type TParam2 = CalculateParam2Type<T1, T2>;
  type TReturn = CalculateReturnType<T1, T2>;

  const result: TReturn = { /* ... */ };

  return result;
}

Notice how I wrote the same types multiple times. It's often worse than that when you have more generic type parameters, and type parameters are named descriptively like TMyValue instead the short T1 etc. The code becomes unreadable quickly. To reduce code duplication, you often have to write intermediate generic types that are only used for that function, and still pass the long list of generics to that type each time.

Typescript should allow us to write the function above like:

function myFunction<T1, T2>(param1: TParam1, param2: TParam2): TReturn {
  type TParam1 = CalculateParam1Type<T1, T2>;
  type TParam2 = CalculateParam2Type<T1, T2>;
  type TReturn = CalculateReturnType<T1, T2>;

  const result: TReturn = { /* ... */ };

  return result;
}

This can be a breaking change, as TParam1 could exist outside of this function for example. In such a case, the type declaration that is outside should be picked to not cause a breaking change.

πŸ’» Use Cases

  1. What do you want to use this for?

Generic functions with complex types would benefit from this change. Library level types often need to handle many cases at once and make invalid states irrepresentable. That often requires complex types, and it goes out of hand without a feature like this.

  1. What shortcomings exist with current approaches?

Current approach is to duplicate the type everywhere it's needed. Duplicate code can lead to bugs. Also code becomes unreadable with large types.

  1. What workarounds are you using in the meantime?

Defining each type declaration multiple times.

@MartinJohns
Copy link
Contributor

MartinJohns commented Feb 21, 2025 β€’

Duplicate of #8704.

What would the declaration file for this even look like? Functions don't contain an implementation anymore, but you can't just pull up the types to the outer scope.

@uhyo
Copy link
Contributor

uhyo commented Feb 24, 2025

What would the declaration file for this even look like? Functions don't contain an implementation anymore

Putting backwards compat stuff aside, you could extend the declration file syntax if really needed πŸ™ƒ:

// .d.ts
function myFunction<T1, T2>(param1: TParam1, param2: TParam2): TReturn {
  type TParam1 = CalculateParam1Type<T1, T2>;
  type TParam2 = CalculateParam2Type<T1, T2>;
  type TReturn = CalculateReturnType<T1, T2>;

  // return result;  ← anything other than type declaration is error inside declaration file function body
}

@KurtGokhan
Copy link
Author

Yes, declaration files are a challenge. If there was a way to do it in DTS files without boilerplate, we would also have a way to do it in TS files. So this feature needs to be added to declarations too somehow.

The way @uhyo proposed looks good to me. It's a syntax change though, so everyone may not be happy about that.

@KurtGokhan
Copy link
Author

KurtGokhan commented Feb 24, 2025 β€’

If we go into the syntax change direction, there are other solutions that may make sense.

To avoid this issue, I sometimes use tricks like this:

function myFunction<
  T1, 
  T2,
  TParam1 extends CalculateParam1Type<T1, T2> = any,
  TParam2 extends CalculateParam2Type<T1, T2> = any,
  TReturn extends CalculateReturnType<T1, T2> = any
>(param1: TParam1, param2: TParam2): TReturn { 
  return {} as TReturn;
}

This is not ideal because, (1) it makes signature more complex, (2) caller may change derived types by mistake, (3) it feels like a hack instead of an intended feature.

If we can add a modifier like derived to the type parameters, and disallow caller to pass them, and drop = any, that could be an ideal solution:

function myFunction<
  T1, 
  T2,
  derived TParam1 extends CalculateParam1Type<T1, T2>,
  derived TParam2 extends CalculateParam2Type<T1, T2>,
  derived TReturn extends CalculateReturnType<T1, T2>
>(param1: TParam1, param2: TParam2): TReturn { 
  return {} as TReturn;
}

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision labels Feb 24, 2025
@RyanCavanaugh
Copy link
Member

This would also be a breaking change, since currently this is legal and means something different:

type Foo = number;
function f(x: Foo) {
  type Foo = string;
}

The existing suggestion to make unspecified type arguments behave differently seems like the right way to try to fix this.

@KurtGokhan
Copy link
Author

I guess you are referring to one of #10571 or #26242 which I am also looking forward to. Fixing those will make new patterns emerge. There may not be need to fix this issue specifically then, as there will be more workarounds. It was still worth a shot to open this issue.

This can be closed as far as I am concerned.

@uhyo
Copy link
Contributor

uhyo commented Feb 25, 2025

By the way, here is a workaround that at least helps you not write the same type twice:

function myFunction<T1, T2>(param1: CalculateParam1Type<T1,T2>, param2: CalculateParam2Type<T1, T2>): CalculateReturnType<T1, T2> {
  type TParam1 = typeof param1;
  type TParam2 = typeof param2;
  type TReturn = ReturnType<typeof myFunction<T1, T2>>;

  const result: TReturn = { /* ... */ };

  return result;
}

@KurtGokhan
Copy link
Author

typeof is a lifesaver but some use cases aren't that simple. For example:

function myFunction<T1, T2, T3, T4>(param1: CalculateParam1Type<CalculateAnotherType<T1,T2,T3, T4>>, param2: CalculateParam2Type<T1, T2>): CalculateReturnType<T1, T2> {
  type TParam1 = typeof param1;
  type TParam2 = typeof param2;
  type TReturn = ReturnType<typeof myFunction<T1, T2>>;
  type TTypeINeed = CalculateAnotherType<T1,T2,T3,T4>; // <-- This

  const result: TReturn = { /* ... */ };

  return result;
}

So unless there is a way to use the generic type parameters before the function signature, there is always a possible use case where you need to repeat code.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Declined" 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 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants