-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Rethinking relationships between {}
type, object
, and primitives
#60582
Comments
duplicate #56205 |
Even if it's a duplicate, given #56205 (comment):
I read this new issue as a request to not do that. As in, now that TypeScript has had better handling around |
Is it just |
Personally, I'm of a "strict" interpretation: that any
This is probably tricky from an implementation perspective. But it's right. |
{}
should not be assignable to object
{}
type should not be assignable to object
@JoshuaKGoldberg So you're suggesting that this shouldn't be allowed: let obj: { length: number } = { length: 10 };
const o: object = obj; Simply because |
If we're unpacking this in detail, negative - your example should be allowed π But the reason why is control flow narrowing, which TS already does in other cases as well. This should be disallowed, though. let obj: { length: number } = { length: 10 };
// ... lots of code
if (Math.random() > 0.5) {
obj = "not an object!"
}
// ... lots of code
const o: object = obj; This should also be disallowed. declare const objOrString: { length: number };
const o: object = objOrString; Note that similar DevX tricks are applied around nullishness, see, for example, const constNullishNumber: null | number = 42;
const num1: number = constNullishNumber; // allowed!
let neverReassigned: null | number = 42;
const num2: number = neverReassigned; // allowed!
// vs
let reassigned: null | number = 42;
if (Math.random() > 0.5) {
reassigned = null;
}
const num3: number = reassigned; // not allowed! |
Yes, that was the point of the comment - it was a counterexample for @JoshuaKGoldberg's original argument. |
...and I'm saying I don't see how it's a counterexample at all? It's just an edge case that would need to be handled by the same sleight-of-hand that TS already includes in similar situations. Regardless, this is perhaps an implementation detail consideration that's not worth spending too many more words going back and forth on before we even find out if this issue will be summarily closed as a duplicate or not π. So I'll leave it there for now to avoid noise in the issue conversation. But definitely an important detail to handle if this does get to the point of implementation! π |
We would need some proposal that creates the desired behavior without unacceptable negative side effects (we've taken several bites at this apple before and not come up with much that was particularly appetizing). No one is going to turn on this behavior if it means every time they write interface Person {
name: string
} they actually need to write interface _PersonFields {
name: string;
}
type Person = object & _PersonFields; to get something that's properly assignable to IMO the right fix here isn't making Or maybe that's some kind of syntax around how the global interface String notextends object {
length: number;
} which would create a type that didn't have intrinsic object-ness. Point being, you should not (generally) have to do extra work to opt in to the existing behavior as relates to being able to assume that types are |
I am enthusiastically on board with considering how to make it so that I guess, before thinking too deeply to reinvent the wheel, I would be pretty interested to know how far down the rabbit hole those of you on the TS team have gone when pondering this previously, and what undesirable edge cases might have arisen. Do you have references on this that I might look into? My first thought is that the overarching model here would be that the runtime JS type, i.e. the result of Since I can't help it, I've started to think through some details and consequences of this, but, as mentioned, probably it's best to catch up to where you are first, before dropping walls of text here π Excited to be having this conversation! |
{}
type should not be assignable to object
{}
type, object
, and primitives
Seems to me that anytime The typescript built-in primitive types should simply automatically remove that modifier from their corresponding reference interfaces internally (I don't think users themselves need the ability to remove that modifier since we can't create new primitive types! There can only ever be the built-in primitives, right?) Without an However, I would also echo @kirkwaiblinger's request for references to any discussion/pitfalls/undesirable edge cases that have already been covered on this topic: would love to read more into it. |
I couldn't find any relevant links (unfortunately this isn't very searchable). The root problem is that the inconsistencies here can't be removed, they can just be moved. We say that this program is legal function foo(s: string) {
return s.length;
} But why is it legal? It's legal because the Similarly, we allow you to write this program, for the same reason: function foo<T extends { length: number }>(x: T) {
return x.length;
}
// Legal call
foo("hello world"); The proposal here implies breaking this consistency: You can no longer take some block of expressions and talk about them in a higher-order fashion if some of those expressions involve property access on primitives. Accessing the This raises further problems, consider something like this const p = "foo";
type X = (typeof p)["length"];
type GetLength<T> = T extends { length: infer U } : U ? never;
type Y = GetLength<typeof p>; Is the type Every place we produce or ingest a property name, you now have to think about whether that property name exists as part of an object syntax or doesn't. e.g. what's This also implicitly breaks common "branding" patterns like I'd also just weigh in that making |
Other issues that became apparent when making an extremely rough prototype (baseline results here) Today Many JSDoc comments use
I think it'd be super weird to have The definition of There's some very real inconsistency around inference of destructuring parameters. Let's say you write function foo({ length }) { }
foo("bar"); This is legal; if you look at the inferred type of function foo(arg: { length: any }) : void so the call would become illegal. IMO that's pretty weird, since if you had written the equivalent downleveling, you'd be fine. Maybe we could iterate through all the primitives and add them to the signature if they'd be compatible and overlappy with the parameter type, but that seems like spooky action at a distance. |
@RyanCavanaugh unless I'm mistaken, all of the polymorphic inconsistency issues as well as the branding pattern/opaque type-alias issues you mentioned only exist in the world where So we come to the problem where the vast majority of functions do not need to assume I suggested You also noted that type X = object { ... } if that would make things clearer! Again though, the number of places where a type signature will actually require |
Another possibility I'd like to throw out to reduce possible boilerplate issues (keeping in mind, as stated above, this is not necessary for the proposal to work--I'm just targeting the only pain-point mentioned): |
π Search Terms
empty object type,
{}
, type safety violation, unsoundπ Version & Regression Information
β― Playground Link
https://www.typescriptlang.org/play/?ts=5.8.0-dev.20241124#code/GYVwdgxgLglg9mABFAhgawKYGcDyAjAKw2gAoAPALkTkOKgEpEBvAKEXcQHpPEAVACxhZEAJxgBzflAA2AT0RDEKPgGVEGESLgiAdGw4xgicogC85xAAZGrDncQQEWONIw6NWkSQDkAxXhEUSH5ELH44EGkAE0Q8DERwEQwUCH4UPFdvegBufXYAXxZClkcwLChqKiZ8sytclm5EACEQCqhBYSEwbygAGiUwGLCI6Ni3FlRMXFpSOBygA
π» Code
π Actual behavior
No error on
takesObject(o)
.{}
can be assigned to typeobject
, despite{}
meaning "all nonnullish values", whereasobject
means only JS object types.π Expected behavior
Error on
takesObject(o)
.{}
is a wider type thanobject
.Additional information about the issue
This stems from a typescript-eslint investigation into making no-unnecessary-condition be more correct around possibly-falsy "object types", such as
{}
, or even{ toFixed(): string}
(to whichnumber
may be assigned). See typescript-eslint/typescript-eslint#10378. This also relates to previous (controversial) conversations about whether to flag the{}
type with the linter, see, e.g. typescript-eslint/typescript-eslint#8700.I'm wondering if this was simply an oversight in #49119, which aimed to fix soundness holes with
{}
?The text was updated successfully, but these errors were encountered: