Skip to content

T implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer #35546

Closed as not planned

Description

TypeScript Version: 3.7.3

Code

type Schema = {
    [key: string]: Number | String | Boolean | (() => Model<any>)
}

type Model<T extends Schema> = {

}

function model<T extends Schema>(type: Schema): Model<T> {
    return undefined as any
}

const User = model({
    id: Number,
    name: String,
    photo: () => Photo
})

const Photo = model({
    id: Number,
    filename: String,
    user: () => User
})

Expected behavior:

No errors. I need this technic to implement automatically inferred circular types. I already have a class-based implementation (you can see example here) and I basically want same with objects of course without manually defining its type. If it's not a bug, any advances on how to implement it keeping type inferred are highly appreciated.

Actual behavior:

Gives following error:

'User' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.(7022)

Playground Link: http://www.typescriptlang.org/play/#code/C4TwDgpgBAygxgCwgWwIZQLxQN4FgBQURUA2gNYQgBcUAzsAE4CWAdgOYC6NAcgK7IAjCAygAfWI1ZsxUAEIB7eQBsIqFjIAUGgJSYAfFACy8gCYQlAHjUg92ggF8CBUJCOnzFgCpQIAD2AQLCa0sIgoqAZYePgOTvgAZrwscMBM8urI7pbefgFBIfBIaHoaLhA0heHaNMZm2QbRxFAMEMC8DOpJZvGsECZQqCHWsTH4cOn0UACqtMKYUJl1Go3ETCY8-EIMADQETSyoyOUSzOy7hMRgCPLA8jQ6+lAACte3Dnaj4yyTLzfy84tzMs9qt1lA+IJhOcmj0VAcjhVJGcQUReLMGPddBgDDNhO8CEA

Activity

  1. mttcr commented on Dec 9, 2019

    @mttcr

    Hello,
    I have another small snippet that shows a similar error:

    class A<T>
    {
    	public readonly b = new B( this );
    }
    class B
    {
    	constructor( a: A<any> )
    	{
    
    	}
    }
    

    tsconfig.json:

    {
    	"compilerOptions": {
    		"outDir": "build",
    		"rootDir": ".",
    		"alwaysStrict": true,
    		"charset": "UTF8",
    		"diagnostics": false,
    		"experimentalDecorators": true,
    		"keyofStringsOnly": true,
    		"noEmitHelpers": true,
    		"noImplicitAny": true,
    		"noImplicitReturns": true,
    		"noImplicitThis": true,
    		"noUnusedLocals": true,
    		"removeComments": false,
    		"skipDefaultLibCheck": true,
    		"skipLibCheck": true,
    		"sourceMap": false,
    		"strictBindCallApply": true,
    		"suppressImplicitAnyIndexErrors": true,
    		"target": "es5",
    		"types": []
    	},
    }
    

    output

    C:\test>node_modules\.bin\tsc -version
    Version 3.7.3
    
    C:\test>node_modules\.bin\tsc
    test.ts:3:18 - error TS7022: 'b' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
    
    3  public readonly b = new B( this );
                       ~
    
    
    Found 1 error.
    

    It works with typescript 3.3.3.
    It starts to fail with typescript 3.4.1.

    The error happened when migrating our codebase from Typescript 3.2.2 to 3.7.3

    The only solution I have for the moment is to change the line to

    public readonly b: B = new B( this );
    
  2. RyanCavanaugh commented on Dec 9, 2019

    @RyanCavanaugh
    Member

    Please only log bugs when a circularity error is issued when there isn't a circularity. These examples both contain plainly-visible cycles and the error is correct.

  3. mttcr commented on Dec 10, 2019

    @mttcr

    Hm, ok, I just commented out because

    1. it was working on a previous version and so need to migrate a 1.5 million LOC typescript codebase
    2. the build randomly fails: at first, I had no build error on my code base, i had some flacky errors in my CI builds (unfortunately, unable to reproduce) (could the error depend on the order of the files are processed? > see below)
    3. The cycle is not that obvious as type B does not depend at all of the parameters of its constructors and is not generic. Type B is straightforward, so is attribute b in Type A, so I was thinking for type A. Sorry for the misunderstanding.

    Also some checks :

    class A<T> { public readonly b = new B( this ); }
    class B { constructor( a: A<any> ) { } }
    

    fails, ok.

    class A<T> { public readonly b = new B( this ); }
    
    class B { 
    	public readonly a: A<any>;
    	constructor( a: A<unknown> ) {
    		this.a = a;
    	}
    }
    

    fails, ok.
    But, if I change the order of declarations of A and B:

    class B { 
    	public readonly a: A<any>;
    	constructor( a: A<unknown> ) {
    		this.a = a;
    	}
    }
    
    class A<T> { public readonly b = new B( this ); }
    

    No build error.

    class B { 
    	public readonly a: A<any>;
    	constructor( a: A<any> ) {
    		this.a = a;
    	}
    }
    
    class A<T> { public readonly b = new B( this ); }
    

    Fails (replaced unknown by any)

  4. MickVanDuijn commented on Dec 20, 2019

    @MickVanDuijn

    This also happens to me when I try to use the aws-sdk

    async function getAllParametersByPath(ssm: AWS.SSM, path: string) {
        let input: AWS.SSM.GetParametersByPathRequest | undefined = {
            Path: path,
        }
    
        const params: AWS.SSM.Parameter[] = []
        while (input) {
            const { Parameters = [], NextToken } = await ssm.getParametersByPath(input).promise()
            params.push(...Parameters)
    
            input = NextToken ? { ...input, NextToken } : undefined
        }
        return params
    }
    

    It infers Parameters: AWS.ParameterList, but gives me the same error on NextToken

    'NextToken' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022)
    

    EDIT: I started out with the below example and that works:

    import AWS from 'aws-sdk'
    
    async function handler() {
        const ssm = new AWS.SSM()
        const { Parameters = [], NextToken } = await ssm
            .getParametersByPath({ Path: '/prefix/' })
            .promise()
    }
    

    Apparently, the circularity is in input? But it is detected only on NextToken (and not on Parameters)

  5. Baldurans commented on Apr 9, 2020

    @Baldurans

    Having similar issue with very simple example

    // ----------
    // ALL OK here, myObject1 has all types detected
    const myObject1 = {
        aaa: 50,
        bbb: () => {
            return myObject1.aaa;
        }
    };
    
    // ----------
    // myObject2 has compilation error: (With strict mode ON)
    // T implicitly has type 'any' because it does not have a type annotation
    //     and is referenced directly or indirectly in its own initializer
    const someFunc = async (input: () => number): Promise<string> => {
        // ...
    };
    
    const myObject2 = {
        aaa: 50,
        bbb: someFunc(() => {
            return myObject2.aaa;
        })
    };
    

    Would really want to know if this is temporary limitation or rather impossible to solve limitation?

    (FYI - ignore that input in someFunc is never called. It can be called in async context. This example is simplified to show the problem)

    Tested with TS 3.8.3

    Playground link: https://www.typescriptlang.org/play/#code/PTAEFpK6FgCgSgIIBkWgPIGlQAsCmATvgDSgC2AnhgEYBW+AxgC4CMeAhgM6gcA2fUM0oAHfDwAm+ZkxkT4jAPYA7LswrV6s9gF5QAb3ihjvDhwBcoAKwAGEkZM0nlgBQBKUDoB8BhyZPEzACuhMoatAwsrAB0ZhwA3H6gAL7wyYlw8IjQOeBZYFQRsgBMnDxK5CIAlnwczFUqoESEioSuAOpVzLigaoRVLBSKUpgAcm75oAAqoFWVfANdfJRlQqL4oADkHMqUm6A0TBxBXBtdoBKK4qDKiuq4HABuGxxrYrzKt8x1DcqT-jsJLMeMQAGZEfDKRj4IESKrEFjLUCtWbKOEI5hIqphLo8RQAdxxyi6VX4VQAXkQFCo1L1FOR8AAxIJQzygFzYkRBZiuDzeG5BciHQhuSx9bEAc08PkMcH8gRCYQARDJaUqMul4NTVOpCloWKU9LKAWZLLZ7HLHM46QzmVCXO5pb5Lf5jArQuF9cxirEzBl-MkJnBNXAgA

    EDIT 1: In our case someFunc is async. Sync can't work, as object is not defined property by property, but all at once. That explains why this is an error. Problem is that it depends how people use it. In sync context it is error for obvious reasons, in async (when at least one tick is passed) it would work fine.

    EDIT 2: Whoever is stuck with this, for us alternative approach worked OK:
    This behaves pretty much same as object definition, but class is created property by property.

    // ----------
    // All types are correctly detected
    const myObject1 = class {
        public static aaa = 50;
        public static bbb = someFunc(() => {
            return myObject1.aaa;
        });
    };
    
  6. vshlos commented on May 1, 2020

    @vshlos

    Small non-circular example. Error shows up on the line that says "error here"

    Removing the line 2 down (the property = records.property, then there is no more error.

    export class Result {
        property?: string
    }
    
    
    export class MyObject {
        async get(property: string): Promise<Result> {
            return new Result();
        }
    }
    
    async function failingTest(queue: MyObject): Promise<void> {
        let property: string | null = null;
        let keepGoing = true;
        while (keepGoing) {
            if (!property) {
                property = "start";
            }
            const records = await queue.get(property); //ERROR HERE
            if (records.property) {
                property = records.property; //NO ERROR IF THIS LINE REMOVED
            }
            keepGoing = true;
        }
    }

    also if I break out the await onto 2 lines, the error goes away:

    ...
            const promise = queue.get(property);
            const records = await promise;
    ...
  7. amatiasq commented on May 31, 2020

    @amatiasq

    Here's another snippet to reproduce it.

    function setter<T>() {
        return <P extends keyof T>(key: P, handler: (self: T, val: T[P]) => void) => {
            return function(this: T, x: T[P]) {
                // complex stuff happens here
                handler(this, x);
            };
        };
    }
    
    class Foo {
        readonly a: string = ''
        setA = setter<Foo>()('a', (self, val) => self)
    }

    Playground

  8. vobarian commented on Jun 16, 2020

    @vobarian

    Simplified example:

    async function getAString(ignore?: string): Promise<string> {
      return "ok";
    }
    
    async function example() {
      let a: string | undefined = "a";
      for (let i = 0; i < 1; i++) {
        const b = await getAString(a); // ERROR
        a = b;
      }
    }

    Note:

    • Error does not happen if the lines are not wrapped in a loop (even though it only loops once)
    • Error does not happen if | undefined is removed on type of a
    • Error does not happen if a = b; is removed or inlined to a = await getAString(a);
    • Error does not happen if getAString is not async

    Playground Link

    I originally ran into this problem trying to use aws-sdk paginated service methods, which often have a NextToken property taking the place of a in my contrived example. The above was just as simplified as I could make it. The actual workaround I used was to declare my variable outside the loop as any.

  9. swansontec commented on Aug 3, 2020

    @swansontec

    I am getting this in an even simpler case:

    const [a, b = a] = [42]

    You can try it out:

    'a' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
    

    However, a should clearly be of type number, based on the way Javascript destructuring works.

  10. tianzhich commented on Aug 27, 2020

    @tianzhich

    I have a similar #40279

  11. bolddp commented on Nov 3, 2020

    @bolddp

    I came across this when the services in my backend needed to reference each other.

    Playground

  12. rajeevnaikte commented on Dec 30, 2020

    @rajeevnaikte

    Same issue troubling me while creating an utility for when-then chaining
    Playground

  13. dk-teknologisk-osv commented on Jan 14, 2021

    @dk-teknologisk-osv

    I have the same issue with await keyword:

    async function main() {
      const exec = (_: () => void): string => ''
      const fn = (_: number) => {}
      const y = await exec(() => fn(y))
      console.log(y)
    }
    

    Without await keyword I get

    Argument of type 'string' is not assignable to parameter of type 'number'.

    With await keyword I get

    'y' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

  14. sk- commented on Mar 30, 2021

    @sk-

    Here's another example with firebase auth and TS 4.2.3.

    const admin = require('firebase-admin');
    
    async function test() {
      const auth = admin.auth();
      // The following 2 assignments work fine
      const result1 = await auth.listUsers(/** maxResult= */ 1000);
      const result2 = await auth.listUsers(/** maxResult= */ 1000, /** pageToken= */ undefined);
      /** @type {string | undefined} */
      let pageToken = 'token';
      while(true) {
        const result3 = await auth.listUsers(/** maxResult= */ 1000, pageToken);
        // If you comment the following line TS does not complain (but the code obviously won't work)
        pageToken = result3.pageToken;
        if (pageToken === undefined) {
          break;
        }
      }
    }

    We are using the same pattern with the gmail api, the only difference there is how the page token is specified. Instead of being it's own argument the gmail api received the pageToken in an options param, as in:

      /** @type {string | undefined} */
      let pageToken;
      while (true) {
        const res = await gmail.users.messages.list({ ...baseRequest, pageToken });
        pageToken = res.data.nextPageToken || undefined;
        if (pageToken === undefined) {
          break;
        }
      }
  15. dustin-rcg commented on Feb 10, 2022

    @dustin-rcg

    Here's another example:

    Code:

    async function main() {
      let lastCursor: string | null = null;
      for (;;) {
        const { ids, cursor } = await request<{ ids: number[]; cursor: string}>({ cursor: lastCursor });
        lastCursor = cursor;
        if (!ids.length) break;
      }
    }
    
    async function request<T>({ cursor }: { cursor: string | null }) {
      return "whatever" as unknown as T;
    }

    Actual result: 'cursor' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.(7022)

    Typescript version 4.5.4

    See Playground

    Comment: This is a common pattern for iterating over paginated results using a cursor. The error goes away if I write

    lastCursor = cursor as any;
  16. ranma42 commented on Apr 6, 2022

    @ranma42

    Another example:

    type Foo = { x: number };
    
    async function bar(arg: Foo): Promise<void> {
      let foo: Foo | undefined = arg;
    
      while (foo) {
        const x = foo.x; // ERROR 7022
        foo = await makeFoo(x);
      }
    }
    
    function makeFoo(x: number): Foo | undefined {
      return Math.random() ? { x } : undefined;
    }

    Playground

  17. ranma42 commented on Jun 24, 2022

    @ranma42

    Ryan Cavanaugh (@RyanCavanaugh) is this snippet considered wrong or should I post it in a separate issue?
    It would be very useful to have some explanation on why the compiler/ide/playground correctly types foo.x as number but is unable to propagate this type to the new const x binding.
    iiuc this has something to do with async/await (namely, the same code snippet compiles just fine if I remove await)

  18. RyanCavanaugh commented on Jun 24, 2022

    @RyanCavanaugh
    Member

    That snippet has a circularity, so it's not a bug if a circularity error is issued.

  19. ranma42 commented on Jun 24, 2022

    @ranma42

    That snippet has a circularity, so it's not a bug if a circularity error is issued.

    The circularity is not very obvious (it looks like in all of the places where it is needed, the code already specifies an explicit type).
    Additionally, it is quite unexpected that adding/removing await changes the circularity.

  20. RyanCavanaugh commented on Jun 24, 2022

    @RyanCavanaugh
    Member

    I don't know what to do with conversations like this. We can avoid some circularity errors by being clever, but there are limits to how much cleverness we're willing to spend. At the limit of that cleverness appears inconsistency. We could fix that by issuing more circularity errors rather than fewer, but I don't think that's what anyone's asking for.

    If you want to dig into the code and send a bugfix-sized PR to address this case, that's totally fine, but we can't treat the issuance of circularity errors in the presence of circularities as a defect.

  21. ranma42 commented on Jun 24, 2022

    @ranma42

    I did not mean to just ask "fix my problem" and I probably failed to explain what I considered the wrong/weird behavior, sorry.

    I agree that if my code is broken just because of circularity issues it is not worth fixing as a special case, but I would expect a consistent behavior between await vs non-await.
    Instead, if the compiler error is a glitch introduced by (some internal desugaring of) await it might be worth investigating.

    In the following snippets I am trying to explain the issue more in detail by expanding (my mental model of) what the compiler should infer about the code.
    Note that I might be very wrong: this would explain why I do not understand the compiler responses; unfortunately the tools I tried (tsc/ide/playground) did not help me much, so I would appreciate a lot the insight of someone who knows the inner details of tsc and/or can provide a different mental model so that I can fix my expectation and reasoning.

    Without await everything works just as I would expect:

    type Foo = { x: number };
    
    async function bar(arg: Foo): Promise<void> {
      let foo: Foo | undefined = arg;
      // I assume that here the compiler knows foo: Foo | undefined; arg: Foo
      while (foo) {
        // I assume that here the compiler knows foo: Foo; arg: Foo;
        const x = foo.x;
        // I assume that here the compiler knows foo: Foo; arg: Foo; x: number
        foo = makeFoo(x);
        // I assume that here the compiler knows foo: Foo | undefined; arg: Foo; x: number
      }
    }
    
    function makeFoo(x: number): Foo | undefined {
      return Math.random() ? { x } : undefined;
    }

    With await apparently my assumptions do not hold true anymore:

    type Foo = { x: number };
    
    async function bar(arg: Foo): Promise<void> {
      let foo: Foo | undefined = arg;
      // I assume that here the compiler knows foo: Foo | undefined; arg: Foo
      while (foo) {
        // I assume that here the compiler knows foo: Foo; arg: Foo;
        const x = foo.x;
        // I assume that here the compiler knows foo: Foo; arg: Foo; x: number
        // in fact, the playground infers `foo.x: number`, but apparently the type is not propagated to `x`
        // instead the compilation fails with an error
        foo = await makeFoo(x);
        // I assume that here the compiler knows foo: Foo | undefined; arg: Foo; x: number
        // but given the previous error, I am unsure if this makes sense at all
      }
    }
    
    function makeFoo(x: number): Foo | undefined {
      return Math.random() ? { x } : undefined;
    }

    Could you help me find out why it behaves differently with/without await?

    I am not very knowledgeable of the internals of tsc, but I would definitely love to help fixing this if it considered a bug in the handling of await 😇 .

  22. RyanCavanaugh commented on Jun 24, 2022

    @RyanCavanaugh
    Member

    Please don't interpret any of this as impatience or offense; I'm not sure why this happens either and also find it odd!

    What I'm trying to say is that circularity errors on circularities are not per se defects, and I'm not going to reallocate finite team resources away from fixing defects or implementing features toward addressing non-defects.

    Could you help me find out why it behaves differently with/without await?

    The investigation itself here is the resource spend that I'm trying to avoid. Figuring out why a thing happens is usually 80% of fixing it.

  23. dustin-rcg commented on Jun 24, 2022

    @dustin-rcg

    I am not very knowledgeable of the internals of tsc, but I would definitely love to help fixing this if it considered a bug in the handling of await 😇 .

    await recursively resolves promises, so it resolves the types T, Promise <T>, Promise<Promise<T>>, ... etc. all down to just T, for some T. I guess that there is a recursive type definition relating to promises (e.g. Awaited<T>) in the typescript codebase, which is where the circularity comes from, but not visible in our code examples.

  24. ranma42 commented on Jun 27, 2022

    @ranma42

    await recursively resolves promises, so it resolves the types T, Promise <T>, Promise<Promise<T>>, ... etc. all down to just T, for some T. I guess that there is a recursive type definition relating to promises (e.g. Awaited<T>) in the typescript codebase, which is where the circularity comes from, but not visible in our code examples.

    I tried reproducing it under this assumption and it looks the issue is not in the recursive type.
    This does not compile even though the await has been replaced with a (trivial) polymorphic identity function.

    type Foo = { x: number };
    
    async function bar(arg: Foo): Promise<void> {
      let foo: Foo | undefined = arg;
    
      while (foo) {
        const x = foo.x; // ERROR 7022
        foo = myawait(makeFoo(x));
      }
    }
    
    function myawait<T>(x: T): T {
      return x;
    }
    
    function makeFoo(x: number): Foo | undefined {
      return Math.random() ? { x } : undefined;
    }

    Conversely, defining

    function myawait(x: Foo | undefined): Foo | undefined {
      return x;
    }

    would make the problem go away.

    Another option is assigning a name to the result of makeFoo(x); this compiles successfully:

    type Foo = { x: number };
    
    async function bar(arg: Foo): Promise<void> {
      let foo: Foo | undefined = arg;
    
      while (foo) {
        const x = foo.x;
        const temp = makeFoo(x);
        foo = await temp;
      }
    }
    
    function makeFoo(x: number): Foo | undefined {
      return Math.random() ? { x } : undefined;
    }

    (the same code works even if makeFoo returns a Promise).
    I guess that depends on how the type inference is driven (around loops and bindings?)

  25. LoganDark commented on May 30, 2024

    @LoganDark

    Posting here because this lint can show up when using default arguments.

    export const X = { // typescript: 'X' implicitly has type 'any' because it does not have a type
    	y: 5,      // annotation and is referenced directly or indirectly in its own initializer. (7022)
    	z(_: number = X.y) {}
    }

    This doesn't happen in a class:

    export class X { // no error
    	static y = 5
    	z(_: number = X.y) {}
    }

    The solution is to move the default into the body of the function:

    export const X = { // no error
    	y: 5,
    	z(w?: number) {
    		w ??= X.y
    		// ...
    	}
    }

    But in JS, the default argument is evaluated at call time, so it's not like Python where X is evaluated while the function object is being created. Even though I suppose it's possible to use something like an IIFE to call the function before X is finished initializing, TypeScript has enough flow analysis to make this an error:

    export const X = { // typescript: 'X' is declared here.
    	y: 5,
    	z: ((w?: number) => {
    		w ??= X.y // typescript: Block-scoped variable 'X' used before its declaration. (2448)
    	})() // <--
    }

    and this not an error:

    export const X = {
    	y: 5,
    	z: ((w?: number) => {
    		w ??= X.y
    	}) // <--
    }

    Default arguments should be handled like that as well, as evaluated only at call time.

    Let me know if I should open a separate GitHub issue for this... since if all flow analysis makes this incorrect assumption, that would be a separate issue. But I think it might be just this lint, which rejects code that would otherwise be accepted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions