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

ReturnType<> broken with never in parameter #33457

Closed
AnyhowStep opened this issue Sep 16, 2019 · 11 comments
Closed

ReturnType<> broken with never in parameter #33457

AnyhowStep opened this issue Sep 16, 2019 · 11 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Sep 16, 2019

TypeScript Version: 3.5.1

Search Terms:

parameter, never, return type inference

Code

type NeverF = (arg : never) => void;

//"y"
type Extends = NeverF extends (...args : any) => any ?
    "y" :
    "n"
;

type TsReturnType<T extends (...args : any) => any> =
    T extends (...args : any) => infer R ?
    R :
    "wtf"
;

//Expected: void
//Actual  : "wtf"
type r = TsReturnType<NeverF>;

//Expected: void
//Actual  : any
type r2 = ReturnType<NeverF>;

Expected behavior:

Both should resolve to void

Actual behavior:

Both resolve to the false branch

Playground Link: Playground

Related Issues:

Did a search for "parameter never in:title" and found nothing


I'm using a function with a parameter of type never because I only care about the return type. And I do not want the user to invoke the function explicitly.

Given the following,

const query2 = myQuery.map((row) => {
  return {
    ...row,
    x : Math.random(),
  };
});

The type of query2 should now be something like,

{
  /*snip*/
  mapDelegate : (row : never, connection : /*snip*/) => (
    { /*snip typeof row*/, x : number }
  ),
  /*snip*/
}

Then,

const query3 = query2.map((row) => {
  return {
    ...row,
    x : row.x + 3.141,
    y : "someNewValue",
  };
});

The type of query3 should now be something like,

{
  /*snip*/
  mapDelegate : (row : never, connection : /*snip*/) => (
    { /*snip typeof row*/, x : number, y : string }
  ),
  /*snip*/
}

I'm also using a never parameter because I want to save on Emit time. It sounds funny but literally every bit of text I shave off helps with compile times.

For large queries, the type of the row parameter could be huuuuuuggeeeee. Like, 100+ lines long.

So,

  1. Only care about the return type
  2. Not meant to be invoked externally
  3. Save on emit time
@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Sep 16, 2019

My workaround at the moment,

type BetterReturnType<T extends (...args : never) => any> =
    T extends (...args : never) => infer R ?
    R :
    never
;

type NeverF = (arg : never, arg1 : any, arg2 : never) => void;

//"y"
type Extends = NeverF extends (...args : any) => any ?
    "y" :
    "n"
;

//Expected: void
//Actual  : any
type existingReturnType = ReturnType<NeverF>;

type BetterReturnType<T extends (...args : never) => any> =
    T extends (...args : never) => infer R ?
    R :
    never
;

//Expected: void
//Actual  : void
type r = BetterReturnType<NeverF>;

//Expected: number
//Actual  : number
type r2 = ReturnType<typeof Math.abs>

//Expected: string[]
//Actual  : string[]
type r3 = ReturnType<(string[])["concat"]>

Playground

@AnyhowStep AnyhowStep changed the title Parameter of type never cases return type inference to fail Parameter of type never causes return type inference to fail Sep 17, 2019
@jack-williams
Copy link
Collaborator

I think this one is subtle, and I'm not sure I have this right, but:

The type (...args : any) => any that is used in the extends type and constraint is classified as an any signature, and all signatures are unconditionally related to an any signature without any structural checking.

The problem is that it's not true that all signatures would be related to an any signature if you did the check structurally, specifically in the case where you use never because never is the only type that any is not related to.

The type NeverF is not structurally related to (...args : any) => any because the inputs are not related (contravariantly), however it gets a free pass because (...args : any) => any is classed as an any signature.

Things appear to work OK for Extends and when checking the constraint of TsReturnType because they both use any signatures, but when you infer and instantiate the extends type in TsReturnType you no longer have an any signature and therefore structural checking is applied --- this fails.

Here is a small explanation:

type NeverF = (arg : never) => void;
type AnySignature = (...args: any) => any;
type NotAnySignature = (...args: any) => unknown;

declare const neverF: NeverF;
let anySignature: AnySignature = neverF; // ok
let notAnySignature: NotAnySignature = neverF; // error, arguments are not related.

Changing the return type causes a type error to appear in the parameters because changing the return type no longer identifiers the signature as an any signature.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 17, 2019
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Sep 17, 2019

@jack-williams is correct. (...args: any[]) => any is very special and won't behave the same as a type the appears to be structurally the same for certain purposes.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Sep 17, 2019

You're saying ReturnType<> shouldn't be changed to the implementation of BetterReturnType<> even though BetterReturnType<> appears to work better?

Maybe I should have renamed the issue to "ReturnType<> broken with never in parameter" instead.

I'm more interested in changing the implementation of ReturnType<> than changing the extends behavior of (...args : any) => any


it gets a free pass because (...args : any) => any is classed as an any signature.

I'm not a fan of that behavior but I'm not too emotionally invested in getting that changed.

It would have been nice to know that I'm not allowed to use ReturnType<> on type (arg : never) => T.

Instead, it was allowed, but failed to work properly (from my pov, even though it's marked as working as intended)

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Sep 17, 2019

Also, just so I'm clear about the "intended" behavior of extends (...args : any) => any,

(arg : never) => T is assignable to (...args : any) => any but not really.

It behaves differently depending on whether it's used to constrain a type parameter, or used in a conditional type, right?


Another reason I use never in parameters is because it's the top type in a contravariant position.

Similar to why people would use unknown in function return types because it's the top type in a covariant position.

Existential types would be nice =x

@jack-williams
Copy link
Collaborator

It behaves differently depending on whether it's used to constrain a type parameter, or used in a conditional type, right?

No it behaves the same. The difference is that in your conditional type the inference of R means you get a type that is altogether different: (...args : any) => void.

All signatures are related to the any signature, independently of where that question is asked in the checker.

It seems to me that if (...arg : never) => infer R can be used in ReturnType without causing any breaking changes that would be an improvement.

@AnyhowStep
Copy link
Contributor Author

Was there a reason to make the false branch return any instead of never?

@AnyhowStep
Copy link
Contributor Author

I tried it.

image

image

I tried it on the playground,

/**
 * Notice the `extends (...args: any` part in the constraint.
 * And the `extends (...args: never` part in the conditional type.
 * 
 * This is intentional.
 */
type BetterReturnType<T extends (...args: any) => any> =
    T extends (...args: never) => infer R ? R : any;
type T19<T extends any[]> = BetterReturnType<(x: string, ...args: T) => T[]>;

//type x = number[][]
type x = T19<number[]>

Playground

So, It seems like the BetterReturnType<> here is the best version?
The only downside is that one failing test.

@typescript-bot
Copy link
Collaborator

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

@AnyhowStep AnyhowStep changed the title Parameter of type never causes return type inference to fail ReturnType<> broken with never in parameter Nov 25, 2019
@AnyhowStep
Copy link
Contributor Author

This was marked as "working as intended" but I don't think it should be intended that "never" in a parameter breaks ReturnType<>

@AnyhowStep
Copy link
Contributor Author

@RyanCavanaugh could you take another look at this please? =(

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
Design Meeting Docket
  
Awaiting triage
Development

Successfully merging a pull request may close this issue.

4 participants