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

Using inferred return type of function as argument to same function breaks type inference #26228

Closed
AnyhowStep opened this issue Aug 6, 2018 · 6 comments
Assignees
Labels
Bug A bug in TypeScript Domain: Type Display Bugs relating to showing types in Quick Info/Tooltips, Signature Help, or Completion Info

Comments

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Aug 6, 2018

TypeScript Version: 3.0.1

Search Terms: return type inference

Code

My specific instance of this problem can be found here
Scroll to the very bottom to see a comment about it.

I couldn't figure out a minimal example but it looks like,

const result1 = foo(/*arg*/); //Inferred type is correct
const result2 = foo(result1); //Inferred type is wrong; shows `any`

However, if I explicitly declare the type of result1, it works,

const result1 : number = foo(/*arg*/); //Explicitly declared
const result2 = foo(result1); //Inferred type is correct

Or, if I do this very weird thing,

//Both foo1<>() and foo2<>() are the same, I just copy pasted foo<>() and renamed
declare function foo1</*Complicated Type*/> (/*Params*/) : /*Complicated Type*/;
declare function foo2</*Complicated Type*/> (/*Params*/) : /*Complicated Type*/;

const result1 = foo1(/*arg*/); //Inferred type is correct
const result2 = foo2(result1); //Inferred type is correct

Trying a type alias fails,

declare function foo</*Complicated Type*/> (/*Params*/) : /*Complicated Type*/;
type FooDelegate = </*Complicated Type*/>(/*Params*/) => /*Complicated Type*/;

const foo1 : FooDelegate = foo;
const foo2 : FooDelegate = foo;

const result1 = foo1(/*arg*/); //Inferred type is correct
const result2 = foo2(result1); //Inferred type is wrong; shows any

But this also works,

declare function foo</*Complicated Type*/> (/*Params*/) : /*Complicated Type*/;

const foo1 : </*Complicated Type*/>(/*Params*/) => /*Complicated Type*/ = foo;
const foo2 : </*Complicated Type*/>(/*Params*/) => /*Complicated Type*/ = foo;

const result1 = foo1(/*arg*/); //Inferred type is correct
const result2 = foo2(result1); //Inferred type is correct

It seems like if I rely on return type inference, I have to copy-paste the function type, and cannot re-use a previous declaration.

So, if I wanted to nest the results 4 times, I'd need foo1, foo2, foo3, foo4

Right now, I just either declare the return-type explicitly, every time the function is used, or I don't use the function at all.

Expected behavior:

Inferred type to not have any

Actual behavior:

Inferred type contains any

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Aug 8, 2018
@RyanCavanaugh
Copy link
Member

Can you boil this down to a standalone example? Just trimming down things from the linked file until there's nothing that doesn't need to be there should be enough. Thanks!

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Aug 19, 2018

It took me forever, but I finally found a minimal example,

type AssertDelegate<T> = (name: string, mixed: unknown) => T;
class Field<NameT extends string, AssertT extends AssertDelegate<any>> {
    constructor(
        readonly name: NameT,
        readonly assertDelegate : AssertT
    ) {}
}
type TypeOf<FieldT extends Field<any, any>> = (
    ReturnType<FieldT["assertDelegate"]>
);
declare function schema<FieldT extends Field<any, any>> (fields : FieldT) : (
    AssertDelegate<{
        [k in FieldT["name"]] : TypeOf<FieldT>
    }>
);

declare const assertBoolean: AssertDelegate<boolean>;
const obj = schema(
    new Field("z", assertBoolean)
);
const objField = new Field("obj", obj);
//Incorrect;
//AssertDelegate<{ obj: any; }>
const actualTypeOfNested = schema(
    objField
);

//OK;
//(name: string, mixed: unknown) => { obj: { z: boolean; }; }
//Or,
//AssertDelegate<{ obj: { z: boolean; }; }>
type expectedTypeOfNested = AssertDelegate<{
    [k in typeof objField["name"]] : TypeOf<typeof objField>
}>

//COPY PASTE of schema<>()
declare function schema2<FieldT extends Field<any, any>> (fields : FieldT) : (
    AssertDelegate<{
        [k in FieldT["name"]] : TypeOf<FieldT>
    }>
);
//OK;
//AssertDelegate<{ obj: { z: boolean; }; }>
//WTF?
const actualTypeOfNested2 = schema2(
    objField
);

//I swear there is some kind of short-circuiting happening here.
//Oh, you are trying to call `foo(foo(/*arg*/))`?
//I'm too lazy, I'll just say the return type is `any`.
//But if you copy-paste and make it,
//`foo2(foo(/*arg*/))`,
//TS goes, "Oh, okay, it's extra work but I'll do it properly"

TS Playground link

I have all the checkboxes ticked in the Options tab; noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitThis, noImplicitReturns


This behaviour is breaking my data validation library that has to validate the data type and value of nested objects in a multitude of ways. Right now, run-time validation works correctly but compile-time checks break because it infers any

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Aug 19, 2018

I just played with it a bit more.

declare function schema<T> (value : T) : (
    {
        field : T
    }
);

declare const b: boolean;
//OK;
//Inferred { field : boolean }
const obj = schema(b);
//Incorrect;
//Inferred { field : any }
//Expected { field : { field : boolean } }
const actualTypeOfNested = schema(obj);

//Gives an error, as expected,
//Type 'boolean' is not assignable to type 'number'
const n: number = actualTypeOfNested.field.field;
//OK
const b2: boolean = actualTypeOfNested.field.field;

//Gives an error, as expected,
//Type 'boolean' is not assignable to type 'number'
const notAssignable: { field: { field: number } } = actualTypeOfNested;
//OK
const assignable: { field: { field: boolean } } = actualTypeOfNested;

TS Playground link

So, it looks like the tooltip that gives the type inference is short-circuiting or something and just giving any. But the actual type checking still goes all the way.

So, it seems like it's somewhat related to #26238

In TS 3.0.1 with VS code (and on the Playground editor), all of a sudden, the tooltip that's supposed to give the inferred type is truncating text, and it looks like it also gives any with nested calls to the same function.

Is there any way I can get it to just display the full type truthfully, without it truncating, or giving up and displaying any? It's kind of a productivity killer when I have to second-guess the tools. They're telling me it's one thing but I check and it's another.

I remember, in TS 2.9, there wasn't truncation on the tooltip displaying inferred types.

@RyanCavanaugh
Copy link
Member

Is there any way I can get it to just display the full type truthfully, without it truncating, or giving up and displaying any?

Fix bugs in the compiler 😉

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript and removed Needs More Info The issue still hasn't been fully clarified labels Aug 20, 2018
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.2 milestone Aug 20, 2018
@RyanCavanaugh
Copy link
Member

@weswigham type display issue

@AnyhowStep
Copy link
Contributor Author

I've tried to look at the source code a few times, cursory glances only, and it just goes over my head. Hopefully I can figure this all out at some point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: Type Display Bugs relating to showing types in Quick Info/Tooltips, Signature Help, or Completion Info
Projects
None yet
Development

No branches or pull requests

3 participants