-
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
TS gets very confused when merging objects inside a generic function #57466
Comments
This isn't a defined behavior; please be specific. You seem to be a little confused on syntax, type T5 = typeof result3["DELETED"]; // = {...}["DELETED"] is parsed as type T5 = (typeof result3)["DELETED"]; // = {...}["DELETED"] so is correctly different from |
Sorry, I'm a little tired as I lost a day because of it during a debug... after having already lost a week trying to fight with other TS typing issues... So, if we have : type Data = Record<string, any>;
type RET_TYPE2 = Omit<Data, "DELETED"> & {DELETED: boolean}; We do agree that no matter what, As well, if we have : function foo<Data extends Record<string, any>>() {
type RET_TYPE = Omit<Data, "DELETED"> & {DELETED: boolean};
} No matter what, function foo<Data extends Record<string, any>>() {
type RET_TYPE = Omit<Data, "DELETED"> & {DELETED: boolean};
let a: RET_TYPE["DELETED"] = false; // TS error
}
Even though let o = {} as RET_TYPE
o.DELETED // <= TS knows this is a boolean. We can also get another error message if we use a
Note: using a type alias: function foo<Data extends Record<string, any>>() {
type RET_TYPE = Omit<Data, "DELETED"> & {DELETE: boolean};
type T1 = RET_TYPE["DELETE"]
let a: T1 = false; // TS error
} Gives less informations :
I don't see why the two should be any different... shouldn't it be 2 ways to get to the same goal ? |
I think I just found out a workaround... I think I understand why it works, but I'm not fully sure. function foo<Data extends Record<string, any>>(t: { [K in keyof Data]: K extends "DELETE" ? never : Data[K] } ) {
type BASE_DATA = {DELETE: boolean};
type RET_TYPE = {
[K in keyof (Data&BASE_DATA)]: (K extends keyof Data ? Data[K] : never) | (K extends keyof BASE_DATA ? BASE_DATA[K] : never)
}
let a: RET_TYPE["DELETE"] = true;
return {} as RET_TYPE;
}
{
let f = foo({a: 32});
f.DELETE // boolean
f.a // number
type Z = typeof f;
type A = Z["DELETE"] // boolean
type B = Z["a"] // number
}
{
let f = foo({a: 32, DELETE: 43});
f.DELETE // does not exists
f.a // does not exists
} |
|
Thanks for your answer. I'll have to check things more in depth tomorrow. Another version that seems to work: function foo<Data extends Record<string, any>>(t: Data) {
type BASE = {DELETED: boolean};
type RET_TYPE = {
[K in keyof (Data&BASE)]: K extends keyof BASE ? BASE[K] : Data[K]
} & {};
let a: RET_TYPE["DELETED"] = true; // TS error
return {} as RET_TYPE;
}
let f = foo({a: 32}); |
I'm not sure you understand what |
Shouldn't this behavior be documented ? or raising a warning when doing so ? I'm even more confused now. Indeed, the following code works without the type Data = Record<string, number>;
type RET_TYPE2 = Data & {DELETED: boolean};
let b: RET_TYPE2["DELETED"] = true; But why ??? Shouldn't And this one doesn't :
EDIT: Doesn't {
type A = { DELETED: boolean }
type B = { DELETED: number }
type C = A&B; // never
}
{
type A = { DELETED: boolean }
type B = Record<string, number>
let b: B = { DELETED: 42 }
type C = A&B; // ok, shouldn't it be never too ?
}
I guess the best workaround we have is : type T = Record<string, any> & {DELETED?: never} The only issue is that it can also take |
According to the documentation:
Then shouldn't the following be correct in this case ? type T = {
DELETED: never // DELETED never occur, i.e. can't be used in type T.
}
let t: T = {} // TS error: missing key DELETED Otherwise, shouldn't the documentation state something like the following instead ?
I also noticed that TS doesn't seem to have a way to express a facultative attribute that can't be undefined... |
You can't document every single small pitfall. And when you look up how
It's an intentional unsoundness to allow combining types with index signatures (that's what What you want would basically require "negated types" (#4196), so you can represent a type "any key, except this one". This is currently not possible. There are many many issues about this use case.
I'm not sure what you mean with "facultative attribute". If you mean an optional property, then this is intentional, because reading a missing property results in |
The issue is that it seems there are a lot of pitfalls, that are hard to understand if we don't know exactly how TS is doing things behind the curtain... and I do not know if we have access to such documentation.
How so ?
How am I supposed to know that if It is maybe obvious for you as you seem to be a contributor, but for a simple user, this isn't.
?
But why doing something unsound, when you could simply add a e.g. I could understand the principle, but at the same time, when doing it inside a generic function, it doesn't work anymore...
It is really hard to use when we get stuck for days due to undocumented unsoundness. And how can we know if something unsound is intentional or is a bug ?
If, indeed, reading a missing property results in For me this is clearly a bug, and I opened an issue for that: |
@denis-migdal the documentation tries to explain concepts so that you can understand how things work at a high level, and assumes that the reader is capable of doing simple experiments if they are curious about edge cases. If you're new to a language I always recommend doing small little tests to understand it better, as well as reading all of the documentation (it's not that long). |
For example, in the documentation for But, in order to use TS properly, we need to know that, or at least to be warned by tsc.
But for that we need to know that theses are edge cases. I got an error, which were unreadable. I spent hours trying to get a minimal reproducible example. |
The docs say
|
Thanks for your answer.
Well, one may assume that a It doesn't explicitly state, e.g. "UnionType, a type defined with the union operator |, /!\ will not work on types like string.". For example:
It seems "properties" here means "explicitly defined properties" as opposed to the "indexed properties". And still, here I use type T = Omit<Record<"toto", number>, string>;
type Z = T["toto"] // TS error (expected) I will have to re-re-read the full documentation, now that I understand more things, and that I have more times for it. |
The docs never imply that strings are unions, nor are any unions in TypeScript infinite as would be required. I don't know how to write documentation in a way that fends off any incorrect assumptions the reader might come in with, unfortunately, and people come up with new incorrect assumptions on a daily basis. I think it'd be tiresome to read docs that spend the majority of their time disclaiming incorrect interpretations.
Of course; TypeScript has thousands of warnings. If you're doing something that doesn't generate a warning, it's probably because that thing is also plausibly some intended thing. Can you be more specific? |
What about some spoilers (e.g. with details/summary HTML tags) : "Show some common misconceptions" ? I don't know, maybe there is something possible to do.
For example, when doing :
In the same way :
|
Mmm... it seems I do have something else going on. In the example below :
I don't know TS internal, but I suggest the following behavior when having such situation :
function foo<T extends Record<string, unknown>>() {
const events = {DELETED: true}//super._events;
const addEvents = {} as T; // simulate
{
type Z2 = {
[K in keyof typeof events]: boolean
}["DELETED"];
const _z: Z2 = true;
}
{
type Z2 = {
[K in (keyof typeof events) | string]: boolean
}["DELETED"];
const _z: Z2 = true;
}
{
type Z2 = {
[K in (keyof typeof events) | (keyof typeof addEvents)]: boolean
}["DELETED"];
const _z: Z2 = true; // ok, but Z2 is {...}["DELETE"] instead of simply boolean
}
{
type Z2 = {
[K in keyof (typeof events & typeof addEvents)]: boolean
}["DELETED"];
const _z: Z2 = true; // ok, but Z2 is {...}["DELETE"] instead of simply boolean
}
{
type Z2 = {
[K in (keyof typeof events) | (keyof typeof addEvents)]: K extends keyof typeof events ? boolean: string
}["DELETED"];
const _z: Z2 = true; // ok, but Z2 is {...}["DELETE"] instead of simply boolean
}
} |
The compiler does not know what type |
It doesn't matter, as no matter what type is really Furthermore : type T = (keyof typeof events) | (keyof typeof addEvents); // = "DELETED" | keyof T
type T = keyof (typeof events & typeof addEvents); // idem So here the compiler at least knows that he can try to precompute the type for the already known "DELETED" property. One of the main advantage, is that error messages will get waaay more clear in such situations. EDIT: And this isn't true, as the compiler does knows it when values are manipulated. |
For future reference, simply adding a |
And that doesn't matter. The compiler does not resolve types that contain unbound generic arguments. Besides that,
Yes, |
Well, it should.
Nope. The only way
It'll be better if we could |
This is an issue only in the case 4: So I stand correct for case 3 and 5... |
π Search Terms
none
π Version & Regression Information
β― Playground Link
Playground Link
π» Code
π Actual behavior
In this context, TS is somehow losing type informations when manipulating types with
[""]
(cf T1 and T5), when he knows the type as shown by T4. In other contexts, TS doesn't lose the type informations as shown by T6.There is also an issue with
Object.assign()
where the returned type is wrong (cf T3,data
can have a member calledDELETED
that will overrideBASE_DATA.DELETED
in theObject.assign()
.)π Expected behavior
TS shouldn't lose type informations and shouldn't bully me like that.
T3 should be any, as
data
can have a member calledDELETED
that will overrideBASE_DATA.DELETED
in theObject.assign()
.Additional information about the issue
No response
The text was updated successfully, but these errors were encountered: