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

Unable to void object keys using strictNullChecks option #8904

Closed
blakeembrey opened this issue May 31, 2016 · 21 comments
Closed

Unable to void object keys using strictNullChecks option #8904

blakeembrey opened this issue May 31, 2016 · 21 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@blakeembrey
Copy link
Contributor

blakeembrey commented May 31, 2016

TypeScript Version:

Nightly

Code

interface Foo {
  userId: string
}

function hey (data: Foo & { userId: void }) {}

hey({ userId: undefined })

Expected behavior: Works. Even better, it would allow me to call it without userId set at all.

Actual behavior:

index.ts(9,5): error TS2345: Argument of type '{ userId: undefined; }' is not assignable to parameter of type 'Foo & { userId: void; }'.
  Type '{ userId: undefined; }' is not assignable to type 'Foo'.
    Types of property 'userId' are incompatible.
      Type 'undefined' is not assignable to type 'string'.

Edit: In case someone misses it in the title, enable strictNullChecks on the nightly releases.
Edit 2: This seems like a big deal since the recommended way of dealing with Object.create(null) is this.
Edit 3: I did also test replacing void with undefined, to the same result.

@Arnavion
Copy link
Contributor

Arnavion commented Jun 1, 2016

Are you sure you don't want a union type Foo | { userId: undefined } instead? I'm not sure how the intersection type { userId: string & undefined } makes sense.

@blakeembrey
Copy link
Contributor Author

blakeembrey commented Jun 1, 2016

@Arnavion Why would a union be useful here? I don't think that would change anything.

I want to be able to use the intersection to override properties of the initial object with valid assignments - in this case, resetting them back to void (not required).

Edit: For example, I have one larger interface and I want to specify that everything other than this property is required.

interface Foo {
  a: string
  b: string
  c: string
  d: string
  ...
}

// Use without `c`
function addsC (foo: Foo & { c: void }) {
  foo.c = 'test'
}

This is a pretty common pattern for me - where I may have a function that defines the ideal object and then some of it can be populated at a later time. For instance, a database model that is currently two objects but after the first object is stored it adds the id to the second object. Currently I have to make a bunch of smaller interfaces and keep extending/intersecting to build them upward - but it requires refactoring interfaces with each change instead of just overriding once.

@Arnavion
Copy link
Contributor

Arnavion commented Jun 1, 2016

But really, the type doesn't make sense. You're trying to do this:

var x: string & undefined;
x = "foo";
x = undefined;

The compiler correctly complains about both the assignments, since neither "foo" nor undefined can satisfy the type string & undefined. The valid values of the type string & undefined need to be simultaneously a string and undefined, which isn't possible.

(Ideally it would complain about the type declaration itself, but it doesn't at the moment.)


It sounds to me like you're actually looking for a way to take an interface of normal properties and produce an interface that has a subset of properties missing or optional, like subtraction types or something like Flow's type operators (examples here).

So hypothetically where \ is the type subtraction operator, you'd write something like:

function addsC (foo: (Foo \ { c: string }) & { c?: string }) {
  foo.c = 'test'
}

or

function addsC (foo: Foo \ { c: string }): Foo {
  return { ...foo, { c: 'test' } };
}

@blakeembrey
Copy link
Contributor Author

blakeembrey commented Jun 1, 2016

@Arnavion Actually, this works on current TypeScript and any nightly - it's only appears if you enable strictNullChecks as I mentioned in the original issue. You can try copying my original snippet into TypeScript and you'll see it actually works.

@blakeembrey
Copy link
Contributor Author

blakeembrey commented Jun 1, 2016

Also, I'm not trying to do string & undefined - I'm doing { x: string } & { x: undefined }.

Edit: That's not to say subtraction wouldn't be useful. I'm trying to point out an inconsistent behaviour with strictNullChecks enabled.

@Arnavion
Copy link
Contributor

Arnavion commented Jun 1, 2016

Of course it works currently without --strictNullChecks, since then string & void is string and undefined is a valid value for string.

And { x: string } & { x: undefined } is the same as { x: string & undefined }

@blakeembrey
Copy link
Contributor Author

blakeembrey commented Jun 1, 2016

Good point. Can you point me to the section talking about the primitive intersection behaviour inside of interfaces? I'd love to understand it better, my understanding of it is currently only intuitive which I would have imagined it's intersected at each property only (not deep) and undefined was assignable to string here.

Edit: I did the tests and understand the intersection of properties (just used string and number). Still, a reference would be helpful for my understanding. I'd love to understand why it's not an error at intersection time and the compiler allows you to create such (invalid) types. I'll try to read up on it this week (outside of TypeScript).

@mhegazy
Copy link
Contributor

mhegazy commented Jun 1, 2016

Edit 2: This seems like a big deal since the recommended way of dealing with Object.create(null) is this.

we did not have undefined type then, so this is why void was recommended. and in hind sight that was not a good work around.

i believe @Arnavion's recommendation is better here. so +1 to Foo | { userId: undefined }

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jun 1, 2016
@blakeembrey
Copy link
Contributor Author

@mhegazy Make sense. Would it be reasonable to make a request that the undefined key doesn't have to be required so {} is assignable to { x: string } | { x: undefined | void }? Since the default empty key of JavaScript is already undefined.

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Jun 2, 2016
@mhegazy
Copy link
Contributor

mhegazy commented Jun 2, 2016

interestingly this works today:

var  o1 : { x: string } | { x: undefined }   = {}; //Fail
var  o2 : { x: string } | { x?: undefined }   = {}; //OK

@Arnavion
Copy link
Contributor

Arnavion commented Jun 2, 2016

{ x: string } | { x?: string } is nicer to read to me.

@blakeembrey
Copy link
Contributor Author

Good to know! Shouldn't x: undefined should act the same then? Considering the 2.0 changes render x?: string as x: string | undefined.

Would be cool to have an improved intro on intersection/union and how to think out it 😄

@Arnavion
Copy link
Contributor

Arnavion commented Jun 2, 2016

Since the default empty key of JavaScript is already undefined.

{} and { x: undefined } do have an observable difference - they can be differentiated by Object.keys or the plain in operator. So I think it's better for TS to continue treating { x: undefined } to mean a required x property of undefined type, and { x?: undefined } to mean an optional x property.

@blakeembrey
Copy link
Contributor Author

Makes sense, I like it. Should be written down in an intersection/union docs page, this is pretty useful information.

I'm going to close this for now. Eventually, I'm guessing, there'll need to be an optional type that doesn't also allow it to be undefined (because Object.keys, as mentioned above) but for now this is perfect for me.

@blakeembrey
Copy link
Contributor Author

Actually, through all this, I just realised it didn't actually answer the question. It still feels inconsistent. Is there somewhere I can read about the intersection and union type implementation and understand why properties inside the intersection don't end up "overriding" with more specific/assignable types instead creating nested intersection types?

I can't actually use the union type discussed above (because, really, it's just made any value valid), but surprisingly you can't also do { x: string } & { x?: string } to make x optional? Is there a reason why?

@blakeembrey blakeembrey reopened this Jun 2, 2016
@blakeembrey
Copy link
Contributor Author

For example, this:

interface Foo {
  point: {
    x: number
    y: number
  }
}

interface Bar {
  point: {
    // x: number
    // y: number
    z: number
  }
}

const x: Foo & Bar = { point: { x: 1, y: 1, z: 1 } }

I just get z is not allow in the literal.

@blakeembrey
Copy link
Contributor Author

blakeembrey commented Jun 2, 2016

Missed this in the above comment:

And when you get rid of z, it's acting as a union (it complains x is not defined on z: number).

Edit: Actually, I'm just going to assume none of this works and the advice is "don't do it". Still, I'd like to understand it better. Should these be errors at the intersection time, at the usage time, should nested intersect as the original comments explained?

@Arnavion
Copy link
Contributor

Arnavion commented Jun 2, 2016

Your example is valid. It's a bug in 1.8.10 that causes it to be rejected. The bug is fixed in master.

@Arnavion
Copy link
Contributor

Arnavion commented Jun 2, 2016

Is there somewhere I can read about the intersection and union type implementation and understand why properties inside the intersection don't end up "overriding" with more specific/assignable types instead creating nested intersection types?

For two types A and B, the type A | B has values that are either valid As or valid Bs. The type A & B has values that are both valid As and valid Bs. There is no "overriding" involved except that which comes from one of the constituents being a subtype of the other (or having properties in common).

If you imagine types as sets, then the union type is the union of the two sets, and the intersection type is the intersection of the two sets. That is where the names come from.

Say you have

type A = { x: string };
let a: A = { x: "x" };

type B = { x?: string };
let b: B = {};

type U = A | B;
let u: U;
u = a; // Valid
u = b; // Valid

type I = A & B;
let i: I;
i = a; // Valid
i = b; // Invalid

Since a and b are respectively valid As and Bs, they're both also valid Us by definition of the union operator.

Since a is a valid A and also a valid B, it's a valid I by definition of the intersection operator. Since b is a valid B but not a valid A, it isn't a valid I.

In fact, B is a (strict) supertype of A since every valid A is a valid B, but not every valid B is a valid A. If you imagine a Venn diagram of the two sets A and B, then A is completely inside B. So the intersection type I is identical to the type A.

@Arnavion
Copy link
Contributor

Arnavion commented Jun 2, 2016

Taking your original example

type A = { x: string };
type B = { x: undefined };
type I = A & B;

What kind of values are valid Is? They have to be valid As, so they have to have an x property that is a string. They also have to be valid Bs, so the x property also has to be undefined. If there were some type T that was both a string and undefined, then x has to be of that type. In other words, the type can be written as

type T = /* something that's both a string and undefined */
type I = { x: T };

What's T? From the definition of the intersection type, we can see it's an intersection of string and undefined.

So type T = string & undefined; and thus type I = { x: string & undefined }

It's not possible to have a value that is both a string and undefined, so this type has no values.

Consider the union type type U = A | B. By the definition, any value that's a valid A is a valid U, so a valid U can have an x property of type string. Any vaue that's a valid B is also a valid U, so a valid U can instead have an x property of type undefined. So if there were some type that had valid values that were either string or undefined, then the x property would be of that type.

So type T = /* something that's either string or undefined */; type U = { x: T }; And again, from the definition of union types, we can see that type T = string | undefined; and thus type U = { x: string | undefined }

@blakeembrey
Copy link
Contributor Author

Cool, thanks! I definitely should have run the latest checks on master, I could have kept it closed 😄 Still, I appreciate the explanation. In this case, have you seen an implementation that does a "composed type" or something similar?

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants