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

strictNullChecks: Object property of class is wrongfully claimed to be possibly undefined #21853

Closed
jacobmadsen opened this issue Feb 10, 2018 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@jacobmadsen
Copy link

jacobmadsen commented Feb 10, 2018

TypeScript Version: 2.8.0-dev.20180210

Search Terms: TS2532

Code

ts2532-test.ts:

type SomeMap<T> = {
  [P in keyof T]: PropertyMap<T, T[P]>
}

type SomeMapper<T> = (map: SomeMap<T>) => void

export default class PropertyMap<T, P> {
  asObject(map: SomeMapper<P>): void {
    map({ street: "street" } as any)
  }
  asString(): void { }
}

class Mapper<T extends object> {
  constructor(map?: SomeMapper<T>) {}
}

class SomePerson {
  name?: string
  address?: { street?: string }
}

class SomePersonMapper extends Mapper<SomePerson> {
  constructor() {
    super(map => {
      map.name.asString()
      map.address.asObject(propMap => {
        propMap.street.asString() // propMap: Error:(28, 9) TS2532: Object is possibly 'undefined'.
      })
    })
  }
}

const mapper = new SomePersonMapper()

tsc --strictNullChecks ts2532-test.ts

Expected behavior:
"propMap" is never undefined.

Actual behavior:
ts2532-test.ts(28,9): error TS2532: Object is possibly 'undefined'.

Playground Link: https://www.typescriptlang.org/play/#src=type%20SomeMap%3CT%3E%20%3D%20%7B%0D%0A%20%20%5BP%20in%20keyof%20T%5D%3A%20PropertyMap%3CT%2C%20T%5BP%5D%3E%0D%0A%20%20%7D%0D%0A%0D%0Atype%20SomeMapper%3CT%3E%20%3D%20(map%3A%20SomeMap%3CT%3E)%20%3D%3E%20void%0D%0A%0D%0Aexport%20default%20class%20PropertyMap%3CT%2C%20P%3E%20%7B%0D%0A%20%20asObject(map%3A%20SomeMapper%3CP%3E)%3A%20void%20%7B%0D%0A%20%20%20%20map(%7B%20street%3A%20%22street%22%20%7D%20as%20any)%0D%0A%20%20%7D%0D%0A%20%20asString()%3A%20void%20%7B%20%7D%0D%0A%7D%0D%0A%0D%0Aclass%20Mapper%3CT%20extends%20object%3E%20%7B%0D%0A%20%20constructor(map%3F%3A%20SomeMapper%3CT%3E)%20%7B%7D%0D%0A%7D%0D%0A%0D%0Aclass%20SomePerson%20%7B%0D%0A%20%20name%3F%3A%20string%0D%0A%20%20address%3F%3A%20%7B%20street%3F%3A%20string%20%7D%0D%0A%7D%0D%0A%0D%0Aclass%20SomePersonMapper%20extends%20Mapper%3CSomePerson%3E%20%7B%0D%0A%20%20constructor()%20%7B%0D%0A%20%20%20%20super(map%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20map.name.asString()%0D%0A%20%20%20%20%20%20map.address.asObject(propMap%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%20%20propMap.street.asString()%20%2F%2F%20propMap%3A%20Error%3A(67%2C%209)%20TS2532%3A%20Object%20is%20possibly%20'undefined'.%0D%0A%20%20%20%20%20%20%7D)%0D%0A%20%20%20%20%7D)%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Aconst%20mapper%20%3D%20new%20SomePersonMapper()

Related Issues:

@RyanCavanaugh
Copy link
Member

This error is correct. The type of propMap is taken from the type of SomePerson.address, which is indeed allowed to be undefined because it's optional.

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 10, 2018
@jacobmadsen
Copy link
Author

jacobmadsen commented Feb 12, 2018

@RyanCavanaugh
I believe you, but I'm trying to understand it. Why is "address" in "map.address" not detected as "TS2532: Object is possibly 'undefined'."

And if you have the time and its possible with TypeScript, how would I go about and map the type, so optional is removed?

@ahejlsberg
Copy link
Member

ahejlsberg commented Feb 13, 2018

@jacobmadsen There's a long thread on the topic of removing optionality in #15012. The Required<T> at the bottom of the thread does the job but still isn't quite right since it doesn't consider Required<T> to be assignable to T. We're looking at ways to address this.

Meanwhile, an alternative strategy for your problem would be to start with types containing properties that are not optional and then add optionality as appropriate (since it is easy to do so).

@jacobmadsen
Copy link
Author

@ahejlsberg Thank you. I believe its enough to make a workable solution to my problem.

@simonbuchan
Copy link

simonbuchan commented Mar 4, 2018

Counter-argument: "bar" in foo doesn't mean the same thing as foo.bar !== undefined - I checked #15012 and didn't see an argument about why this is TS's behavior in the first place, and didn't see any discussion about this logic in the design notes here (though optional properties pre-date undefined type).

For example, with the new in guard, the following should work:

declare const x: { y?: number };

if ("y" in x) {
    const y: number = x.y;
}

but currently fails with undefined is not assignable to number. Of course, that means that x.y = undefined must not be legal, so perhaps the compat cost is too high.

Of course, the type of (rvalue) x.y should (absent type guards) still be number | undefined, but that's a property access expression type, not a property type,

@omidkrad
Copy link

Is it the same issue? Why is car1 fine but car2 gives an error?

// strictNullChecks: true

interface Car {
  wheels?: number[];
}

const car1: Car = {}
car1.wheels = []; // set property outside of object literal

const numWheels1 = car1.wheels.length; // no error

const car2: Car = {
  wheels: [],
}

const numWheels2 = car2.wheels.length; // Error: object is possibly undefined.

@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants