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

Fix #1809, introduce non primitive object type #12501

Merged
merged 10 commits into from
Jan 6, 2017

Conversation

HerringtonDarkholme
Copy link
Contributor

@HerringtonDarkholme HerringtonDarkholme commented Nov 25, 2016

Fixes #1809. I found object primitive type is quite confusing. Because what it means is that other primitive types cannot be assigned to. So I change the name to non primitive.

non primitive type is quite useful for ECMA API like Object.create, Object.setPrototype and Proxy.
It is also useful for userland libraries such as immutable and backbone.

@@ -1662,6 +1663,13 @@ namespace ts {
return type;
}

function createNonPrimitiveType(): ResolvedType {
const type = setStructuredTypeMembers(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahejlsberg can judge better, but I don't think you want to set any structured type members - I think you want to just need to add a branch in getApparentType for object.

@@ -3042,6 +3053,10 @@ namespace ts {
return type && (type.flags & TypeFlags.Never) !== 0;
}

function isTypeNonPrimitive(type: Type) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used in one place right now, so consider just inlining.

@DanielRosenwasser
Copy link
Member

@HerringtonDarkholme thanks for the PR! As a heads up, we'll probably have to discuss this more at a design meeting, but I think we can iterate on it for a bit.

I think that a change like this would need the following tests:

  • Narrowing from object
    • to an object type (e.g. instanceof, type predicates)
    • to a primitive (e.g. typeof foo === "number", type predicates)
  • Narrowing from object | null | undefined (under strictNullChecks)
    • With == null
    • With === null
    • With typeof foo === "object"
    • With typeof foo === "undefined"
  • Assigning (under strictNullChecks)
    • A null to something of type object.
    • A undefined to something of type object.
    • Something of type Number, String, or Boolean to something of type object.
  • Trying to access members (under noImplicitAny and strictNullChecks)
    • that exist on Object.prototype (e.g. toString, hasOwnProperty)
    • that don't exist on Object.prototype (e.g. abcdef)

Most of these should all run under noImplicitAny and strictNullChecks personally.

@HerringtonDarkholme
Copy link
Contributor Author

Thanks for your quick response and kind suggestion!

I'm quite unsure about the behavior of narrowing behavior.

For example, an empty class like class A {}, should instanceof narrow object to A? Then other primitives will be assignable to object in that branch. Also, should typeof foo === "number" yield never type? I think that will be reasonable.(But I didn't add that test since #1809 does not specify it and TS has changed a lot since then.)

Thanks for the review! I will research other issues!

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Nov 25, 2016

For example, an empty class like class A {}, should instanceof narrow object to A? Then other primitives will be assignable to object in that branch.

If I'm not mistaken TypeScript uses the declared type (i.e. the original type) when getting the left side of an assignment. In those cases, the type should still be object, which should in turn should save you.from that concern, but it'd definitely be very helpful to have a test for this.

@alitaheri
Copy link

This is a great feature, specially when dealing with generic constraints. Can it fully cover the bellow case?

interface Proxy<T extends object> {
  // ...
}

// fails:
const a: Proxy<number> = { ... };
const c: Proxy<null> = { ... };
const c: Proxy<undefined> = { ... };

// won't fail:

interface Blah {
  foo: number;
}

const b: Proxy<Blah> = { ... };

@HerringtonDarkholme
Copy link
Contributor Author

Thanks for all your valuable suggestion!
@alitaheri Proxy<null> will only fail under strictNullChecks. I think a test is needed here.

@dead-claudia
Copy link

dead-claudia commented Dec 13, 2016

Question: what about functions? They're also reference types that can be Object.created and Object.assigned to.


Edit: I don't know what I'm talking about... they implicitly subtype Function.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the semantics but I agree with @DanielRosenwasser that (1) we need to have the whole team discuss at a design meeting (2) nonPrimitiveType should be a simple marker type, more like stringType, numberType etc and then add a branch in getApparentType. IntrinsicType with TypeFlags.Object might even work.

@@ -0,0 +1,31 @@
tests/cases/conformance/types/nonPrimitive/nonPriimitiveInFunction.ts(12,12): error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'object'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in filename: Priimitive

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments on the tests

var x = {};
var y = {foo: "bar"};
var a: object;
x = a;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about a = x and a = y? I think neither should have errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

~~~~~~
!!! error TS2344: Type 'number' does not satisfy the constraint 'object'.
var y: Proxy<null>; // ok
var z: Proxy<undefined> ; // ok
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 51 and 52 are only ok because strictNullChecks is not turned on, right?

Copy link
Contributor Author

@HerringtonDarkholme HerringtonDarkholme Dec 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and there are corresponding tests in nonPrimitiveStrictNull.ts which produces errors.



==== tests/cases/conformance/types/nonPrimitive/nonPrimitiveInGeneric.ts (7 errors) ====
function generic<T>(t: T) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except for null and undefined, I don't think these tests are that valuable, since they are just testing assignability again

@HerringtonDarkholme
Copy link
Contributor Author

HerringtonDarkholme commented Dec 16, 2016

@sandersn Pardon my question.
I have tried to replace nonPrimitive type by createIntrinsicType(TypeFlags.Object, "object"). But I get more errors in objectTypeRelatedTo, isFunctionObjectType and etc, where nonPrimitiveType is used as target and is directly compared rather than its apparent type. Currently I create nonPrimitiveType by reusing TypeFlags.Object so that all object type checking logic is reused.

I wonder if nonPrimitiveType can be labelled as intrinsic. One intrinsic type needs to use one bit flag like Number or String which I think is too expensive for non primitive checking.

Maybe I can change the TypeFlags? Or patching more functions?

@sandersn
Copy link
Member

Yeah, I talked to @ahejlsberg and he said that object needs a new TypeFlags entry of its own. This probably means that you'll have to handle it specially in more places. I think, for most of these places, it's a good idea to have object-specific code in place, though.

@HerringtonDarkholme
Copy link
Contributor Author

Thanks for the clarification! I'm quite hesitant about adding TypeFlags since TypeScript will have more features like variadic type parameter and exact type, which all need TypeFlag entries.

I think I will wait for your whole team discussion.

@HerringtonDarkholme
Copy link
Contributor Author

HerringtonDarkholme commented Dec 19, 2016

I have updated the implementation to use intrinsic type. One thing I'm not sure is whether intrinsic object should be counted as StructuredType. It seems StructuredType flag is used in narrowing type, so adding nonPrimitive to StructuredType enables narrowing object by instanceof.

But if intrinsic type is used in resolveStrucutredTypeMembers, intrinsic type will be returned as resolvedType while it isn't. (I didn't managed to create a test case to cover that.)

I'm very grateful to you for all your review! 😃

Update:
TSLint breaks CI. Awaiting #13006

@sandersn
Copy link
Member

I don't think you should re-use StructuredType either -- it's a generalisation of Object that includes any type that will eventually have members but may not yet. I think the object intrinsic should stay separate.

@HerringtonDarkholme
Copy link
Contributor Author

Thanks for your advice! I have changed nonPrimitive to Narrowable from StructuredType.
Also rebased.

@sandersn sandersn merged commit e9e7fce into microsoft:master Jan 6, 2017
@sandersn
Copy link
Member

sandersn commented Jan 6, 2017

Thanks so much @HerringtonDarkholme!

(And sorry for the delay from the holidays.)

@glazar
Copy link

glazar commented Feb 21, 2017

One common task is to take an existing type and make each of its properties entirely optional.
Thus we have support for Partial since 2.1.

The issue with using Partial is that any usages of Partial also accepts primitives, even though the intent is that primitives be excluded.

Is this new object type, added with 2.2, used by default when using Partial construct ?

It would make sense to make use of it, since it would solve the issue of usages of Partial also accepting primitives.

@sandersn
Copy link
Member

@glazar That seems reasonable. Can you create an issue for that so that we can track it separately? The PR should be simple.

@gcnew
Copy link
Contributor

gcnew commented Feb 21, 2017

If that were the case, wouldn't it break the currently used "deep partial" implementations? The same applies to Readonly, I think.

@sandersn
Copy link
Member

@gcnew can you give an example of a deep partial type? The Partial<T> in lib.d.ts looks shallow to me.

As I recall, the basic problem with a recursive partial type is that we have no way to stop the recursion right now, so it does in fact recur right though primitives, chopping them up into length, charAt, charCodeAt, etc. Eventually I suspect that mapped types will add conditionals so you can stop the recursion upon reaching primitives.

@glazar
Copy link

glazar commented Feb 22, 2017

@sandersn @gcnew
I have created a dedicated issue for the situation I mentioned in my previous comment - #14224

@gcnew
Copy link
Contributor

gcnew commented Feb 22, 2017

@sandersn I don't have anything specific in mind. I think I've seen people asking question and submitting hacky code that might be affected. On a more serious note, will it work correctly if the target type has type parameters?

@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
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants