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

Less restrictive object type #18228

Closed
saschanaz opened this issue Sep 3, 2017 · 20 comments
Closed

Less restrictive object type #18228

saschanaz opened this issue Sep 3, 2017 · 20 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@saschanaz
Copy link
Contributor

saschanaz commented Sep 3, 2017

New behavior

No property existence check for object type.

let foo: object;
foo.bar; // no error

Rationale

The current object type is useful when used to type function arguments but not very useful on object members. See this IDL:

[SecureContext]
interface PaymentResponse {
    [Default] object        toJSON();

    readonly attribute DOMString       requestId;
    readonly attribute DOMString       methodName;
    readonly attribute object          details;
    readonly attribute PaymentAddress? shippingAddress;
    readonly attribute DOMString?      shippingOption;
    readonly attribute DOMString?      payerName;
    readonly attribute DOMString?      payerEmail;
    readonly attribute DOMString?      payerPhone;

    Promise<void> complete(optional PaymentComplete result = "unknown");
};

Here, the details member can be any "object" depending on payment method behavior. Current lib.d.ts defines this member as any so that a user can access any untyped members.

let response: PaymentResponse;
let { details } = response;
details.anImplementationDependentMemberName; // great

But any means it can be boolean, number, string or even null or undefined.

if (typeof details === "number") {
  // `details` is resolved as `number` but really should be `never` here
}
details++; // no error, :(

A language service should be able to warn on these cases while still allowing arbitrary member accesses. Allowing such free accesses on object type will help here:

details.anImplementationDependentMemberName; // still great
if (typeof details === "number") {
  // it cannot be a number, so `never` here
}
details++; // error now :D

Hey, a function can also be an object in TS!

Yes, but I don't think the fact will cause any problem as functions can also have arbitrary members. And I'm not proposing arbitrary function call.

@NN---
Copy link

NN--- commented Sep 4, 2017

See also #10715 and #12416

@mhegazy
Copy link
Contributor

mhegazy commented Sep 5, 2017

Looks like this should be covered by #10715

@mhegazy mhegazy added the Duplicate An existing issue was already created label Sep 5, 2017
@saschanaz
Copy link
Contributor Author

saschanaz commented Sep 8, 2017

I think the main difference between this and the proposed unknown type is that unknown never ensures that the object is a non-primitive one, so I don't think this is a duplicate of #10715.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 11, 2017

I do not think #10715 is about unknown. it is not clear what unknown really means. i think it is mainly about adding properties to a type by asserting them. if were to do this we would do it on object. I am assuming this is the underlying reason for this proposal, wanting to check for a JSON object properties, and assert their existence.

Asking for unchecked property access on object but not wanting to use any seems a bit unbalanced to me. details.foo++ is more or less the same as details++.

@saschanaz
Copy link
Contributor Author

I'm not proposing adding properties by assertion, I'm rather proposing a type starting as object but doesn't have property existence check:

var o: object;
o.anything.is.possible.without.assertion;

var n: number;
n = o; // but not this, because `o` is a non-primitive object.

This basically makes object as "a union type of all possible object types", when any is a union type of all types.

@mhegazy mhegazy added Suggestion An idea for TypeScript and removed Duplicate An existing issue was already created labels Sep 12, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Sep 12, 2017

so why not any? your argument about safety is compromised by making object property access unchecked.

@saschanaz
Copy link
Contributor Author

I want to keep the type safety of the base object type, not the types of its properties.

function foo(bar: object) {
  return bar;
}

// Here, you can put anything on the argument if it is a non-primitive object
const f = foo({ arbitrary1: 0, arbitrary2: 1 });

// Now f is an object with arbitrary properties, but suddenly TS warns
f.arbitrary1
//~~~~~~~~~~ Property `arbitrary1` does not exist on type `object`.

If I do any:

f.arbitrary1 // No problem

1 + f.arbitrary1; // Okay, do it
1 + f; // ??

@mhegazy
Copy link
Contributor

mhegazy commented Sep 12, 2017

not sure i follow..

var f: object;

f.arbitrary1 // Error: Property 'arbitrary1' does not exisit on type 'object'

1 + f.arbitrary1; // still an Error
1 + f; // Error: Operator '+' can not be applies to '1' and 'object'

this seems uniform to me.

@saschanaz
Copy link
Contributor Author

saschanaz commented Sep 13, 2017

this seems uniform to me.

Yes, but that's not a useful behavior. It just acts as an empty interface {} does.

var f: {};

f.arbitrary1 // Error: Property 'arbitrary1' does not exisit on type 'object'

1 + f.arbitrary1; // still an Error
1 + f; // Error: Operator '+' can not be applies to '1' and 'object'

My proposal here is to represent an object type that can have any properties, say a user-defined configuration object bag that is later exposed as a member.

BTW, @HerringtonDarkholme, what are the key differences between object and {[k: string]: any} when it is used on function arguments? The code in TS2.2 release note works also with the latter.

declare function create(o: {[k: string]: any} | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

@mhegazy
Copy link
Contributor

mhegazy commented Sep 14, 2017

Yes, but that's not a useful behavior. It just acts as an empty interface {} does.

well, it is safe. if you want the open to all kinda object, use any.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 14, 2017

My proposal here is to represent an object type that can have any properties, say a user-defined configuration object bag that is later exposed as a member.

how is that different from { [x:string]: any }?

@mhegazy mhegazy added Needs More Info The issue still hasn't been fully clarified and removed Suggestion An idea for TypeScript labels Sep 14, 2017
@saschanaz
Copy link
Contributor Author

saschanaz commented Sep 15, 2017

how is that different from { [x:string]: any }?

That's my new question, what's the purpose of object type when it is for function arguments (at least #1809 and TS2.2 release note describe so) and { [x:string]: any } can be used there?

@mhegazy
Copy link
Contributor

mhegazy commented Sep 15, 2017

object is useful in an input position to signal something that is not a primitive. it is also useful when you have something you want to treat like a black box. { [x:string]: any} has more assumptions about the type. namelly that all its properties is of type any. it is similar to the difference between {} and any.

@saschanaz
Copy link
Contributor Author

saschanaz commented Sep 16, 2017

If one wants to use object to type functions written in TS then it makes sense, but the discussion on #1809 and the OP on #12501 show that it was to type existing JS functions including Object.create.

Why did TS team decide to introduce a new type when we can use static create(proto: { [x:string]: any }); and the resulting behavior is same?

In other words, do these functions have different behaviors?:

declare function foo(input: object): void;
declare function foo(input: { [key: string]: any }): void;

@mhegazy
Copy link
Contributor

mhegazy commented Sep 18, 2017

they are close, but they are not the same. object is more restrictive than { [key: string]: any } see #1809 for more details.

@saschanaz
Copy link
Contributor Author

Still not sure, can you kindly give me an example where declare function foo(input: { [key: string]: any }): void; accepts when declare function foo(input: object): void; rejects?

@mhegazy
Copy link
Contributor

mhegazy commented Sep 19, 2017

for a function parameter they should behave the same way. however, a function taking { [x:string]: any } contract allows for accessing its members, where as object is more of a black box. it depends on what you want to do with them. if is it is a property bag that you expect use { [x:string]: any }, if it is an opaque object that you just want to hold on for identity use object. for constraints to ensure that you are not getting number or string, use T extends object just cause it is shorter and more descriptive.

@saschanaz
Copy link
Contributor Author

Hmm, I see. Thank you for your explanation.

@RyanCavanaugh
Copy link
Member

Seems like we should have a FAQ entry for the differences between {}, { [s: string]: any }, object, and any

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

6 participants
@NN--- @HerringtonDarkholme @saschanaz @RyanCavanaugh @mhegazy and others