You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Inconsistent type validation: Different result depending on inline defined and executed function and declared and executed function #59560
consttest=(value: string[]): void=>{};// Works Oktest((()=>["test"]asconst)());// Fails (while it's equivalent to first)constresolveArgs=()=>["test"]asconst;test(resolveArgs());
🙁 Actual behavior
The first invocation is accepted as one without issues. The second which is equivalent, is rejected with the:
Argument of type 'readonly ["test"]' is not assignable to parameter of type 'string[]'.
The type 'readonly ["test"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.(2345)
Different inferences in the face of different contextual types doesn't seem like a bug.
I think it all depends on what are our expectations from this library
I believe we all agree, both executions are equivalent (in sense that in both cases test function receives input that's exactly same typewise), yet they are recognized differently. Why?
It feels as either logical flaw or logical limitation in Typescript.
Could you fill out the issue template completely -- is this a regression? And if so, from when?
All I know is that it happens in v5.5.4. I'm new to the TypeScript so it's hard for me say, if it's something new, or by design for a long time
In the first example you pass the function directly to test, so the compiler can infer based on the context that the value ["test"] as const should be typed as string[].
In your second example the function is on its own. The compiler does not know that it's supposed to be used as a string[] later, so it's using the narrow type readonly ["test"] (because of the as const).
This is neither a logical flaw, nor a logical limitation, but working as intended.
so the compiler can infer based on the context that the value ["test"] as const should be typed as string[].
Why can't the compiler infer the same information when the function is passed on its own?
The implementation of this function is just as accessible as of one passed directly and the compiler can read through it in the same way.
It's clear that both versions work exactly the same, and nothing different can happen in either case. If the compiler is unable to deduce what is evident in these few lines of code, it seems like a limitation on the compiler's side.
Because resolveArgs can be used further down in the code, so it can't use the contextual type information on one specific call site.
This could be somewhere else in the code, and if the type of resolveArgs is inferred based on the usage in test, then it wouldn't be compatible with foo anymore. Since the compiler can't know where the function will be used, it can't use a more specific type and uses the information that is available (the as const).
The compiler can clearly see that this function is not used anywhere else. However, let's put that aside. I understand that there might be an agreement not to make the compiler work in a way where newly introduced code (in this case, additional function usage) changes how previous code is validated.
Now, taking it further, I still don't understand how ["test"] as const can be interpreted differently in both cases. Isn't it equivalent to readonly ["test"] in all cases?
If I understand correctly, you are stating that in the case of test((() => ["test"] as const)());, the ["test"] as const is not inferred as equivalent to readonly ["test"]. Is that correct?
Why is ["test"] as const considered a valid input in the first usage but no longer considered as such in the second one? This value is not subject to represent anything else under any circumstances
It feels as either logical flaw or logical limitation in Typescript.
I think there are four-ish logical endpoints for this kind of analysis:
No-helping mode, where even obviously-okay code is still rejected if it would also be rejected under indirection
Single-order, the kind that you see today where immediately-observed expressions can get contextual treatment, but not indirectly-referenced ones
A hazy middle ground full of inconsistencies where things work at e.g. four levels of indirection and not five, which is hard to tease out because the differences only start appearing in large samples
Infinite analysis that can detect every possible indirection/non-indirection
Option 3 is not something that's tractable to do with reasonable performance. Option 0 also seems bad; people would complain a lot if obviously-fine code errored for no good reason. So you're asking why we don't do option 2 vs option 1, and I think the description of option 2 explains why
Thanks, Ryan Cavanaugh (@RyanCavanaugh), for this insight. It sounds very reasonable. I can imagine that aiming for option 2 would be like opening Pandora's box.
Agreeing that we're on option 1, I still have trouble understanding why the above fails. Let's group some facts:
resolveArgs unconditionally returns ["test"] as const. No matter where or how many times it's used, it's the outcome, and the compiler can easily infer that (no need to go beyond "1")
test declares that it will accept any array of strings.
["test"] as const is a valid member of the set of all possible values that will satisfy the string[] declaration. Why, then, is this match reported as invalid?
Why does introducing a second function that takes ["test"] as const, but which declares readonly ["test"]) as the only valid input, changes our validation approach to a function which accepts string[]
The bare inference of ["test"] as const is readonly ["test"]. Note that it's readonly, and you're trying to pass it to a function that takes a mutable array. That's not legal.
When an as const'd array literal appears in a place where a mutable array is expected, it's allowed to subtype back to the mutable version of the array, since it can be seen that no one else has a reference to the array that would become unsound after the array is mutated.
Note that it's readonly, and you're trying to pass it to a function that takes a mutable array. That's not legal.
Indeed, I missed that in the above reasoning. Technically test assumes rights to mutate the input, therefore readonly input should be rejected.
it's allowed to subtype back to the mutable version of the array,
This is an actual source of confusion. I think the fact that the given array is not referenced anywhere else shouldn't dismiss that it was declared as const.
I think the fact that the given array is not referenced anywhere else shouldn't dismiss that it was declared as const
Why? That breaks code like this:
declarefunctionfoo<T>(arg: readonlyT[]): T[];// p1: string[], but I wanted ("one" | "two")[]constp1=foo(["one","two"]);// Actual: p2: ("one" | "two")[] ✅// Proposed: Error here instead, but// what should the user write instead?constp2=foo(["one","two"]asconst);
Why is mutation on ["test"] as const allowed conditionally?
See above example - a "fresh" const array literal can be safely treated as mutable
Doesn't ["one", "two"] as const satisfy readonly T[] on its own (without downgrading to mutable)?
I see that foo takes a readonly array of elements of type T and returns a mutable array of elements of type T. I believe there's no assumption that the input and output are the same objects; the only assumption is that they are of the same type. Right?
declarefunctionfoo<T>(arg: T[]): T[];// p1: string[], but I wanted ("one" | "two")[]constp1=foo(["one","two"]);// Actual: p2: ("one" | "two")[] ✅// Proposed: Error here instead, but// what should the user write instead?constp2=foo(["one","two"]asconst);
Why exactly there's a room for any error at p2 case?
I read it as: foo receives the tuple of literal types "one" and "two", therefore the expected output structure is ("one" | "two")[].
Similarly as if I'd pass [1, 'string'], the expected output structure would be (number | string)[].
I'm not sure how it explains that in previous example we allow mutation for ["test"] as const input
This issue has been marked as "Question" and has seen no recent activity
?? That's interesting. So if I do not respond to the comment on the issue during the weekend the issue will be autoclosed? :) Where's the work/life balance thing? ;-)
The proposed behavior is that ["one", "two"] as const be inferred as a readonly array, but it's illegal to pass a readonly array to foo
Thanks for responding. I see now that mutability is tied to type declarations. In this case, the input was declared as T[], indicating a mutable array of type T. That part wasn’t entirely clear to me at first.
what should the user write instead?
I guess ("one" | "two")[] could work in that case(?), but I understand that it would be both inconvenient and error-prone to list all possible items by hand.
Therefore, if there's no convenient syntax to pass this information (as mutable), I assume TypeScript intentionally allows a hack where the user can pass an immutable that can be subtyped into a mutable.
Activity
nmain commentedon Aug 8, 2024
Different inferences in the face of different contextual types doesn't seem like a bug.
Could you fill out the issue template completely -- is this a regression? And if so, from when?
medikoo commentedon Aug 8, 2024
I think it all depends on what are our expectations from this library
I believe we all agree, both executions are equivalent (in sense that in both cases
testfunction receives input that's exactly same typewise), yet they are recognized differently. Why?It feels as either logical flaw or logical limitation in Typescript.
All I know is that it happens in v5.5.4. I'm new to the TypeScript so it's hard for me say, if it's something new, or by design for a long time
MartinJohns commentedon Aug 8, 2024
In the first example you pass the function directly to
test, so the compiler can infer based on the context that the value["test"] as constshould be typed asstring[].In your second example the function is on its own. The compiler does not know that it's supposed to be used as a
string[]later, so it's using the narrow typereadonly ["test"](because of theas const).This is neither a logical flaw, nor a logical limitation, but working as intended.
medikoo commentedon Aug 8, 2024
Why can't the compiler infer the same information when the function is passed on its own?
The implementation of this function is just as accessible as of one passed directly and the compiler can read through it in the same way.
It's clear that both versions work exactly the same, and nothing different can happen in either case. If the compiler is unable to deduce what is evident in these few lines of code, it seems like a limitation on the compiler's side.
MartinJohns commentedon Aug 8, 2024
Because
resolveArgscan be used further down in the code, so it can't use the contextual type information on one specific call site.This could be somewhere else in the code, and if the type of
resolveArgsis inferred based on the usage intest, then it wouldn't be compatible withfooanymore. Since the compiler can't know where the function will be used, it can't use a more specific type and uses the information that is available (theas const).The second version introduces a new variable, the first one not. It's a major difference.
medikoo commentedon Aug 8, 2024
The compiler can clearly see that this function is not used anywhere else. However, let's put that aside. I understand that there might be an agreement not to make the compiler work in a way where newly introduced code (in this case, additional function usage) changes how previous code is validated.
Now, taking it further, I still don't understand how
["test"] as constcan be interpreted differently in both cases. Isn't it equivalent toreadonly ["test"]in all cases?If I understand correctly, you are stating that in the case of
test((() => ["test"] as const)());, the["test"] as constis not inferred as equivalent toreadonly ["test"]. Is that correct?Why is
["test"] as constconsidered a valid input in the first usage but no longer considered as such in the second one? This value is not subject to represent anything else under any circumstancesRyanCavanaugh commentedon Aug 8, 2024
I think there are four-ish logical endpoints for this kind of analysis:
Option 3 is not something that's tractable to do with reasonable performance. Option 0 also seems bad; people would complain a lot if obviously-fine code errored for no good reason. So you're asking why we don't do option 2 vs option 1, and I think the description of option 2 explains why
medikoo commentedon Aug 8, 2024
Thanks, Ryan Cavanaugh (@RyanCavanaugh), for this insight. It sounds very reasonable. I can imagine that aiming for option 2 would be like opening Pandora's box.
Agreeing that we're on option 1, I still have trouble understanding why the above fails. Let's group some facts:
resolveArgsunconditionally returns["test"] as const. No matter where or how many times it's used, it's the outcome, and the compiler can easily infer that (no need to go beyond "1")testdeclares that it will accept any array of strings.["test"] as constis a valid member of the set of all possible values that will satisfy thestring[]declaration. Why, then, is this match reported as invalid?Why does introducing a second function that takes
["test"] as const, but which declaresreadonly ["test"])as the only valid input, changes our validation approach to a function which acceptsstring[]RyanCavanaugh commentedon Aug 8, 2024
The bare inference of
["test"] as constisreadonly ["test"]. Note that it's readonly, and you're trying to pass it to a function that takes a mutable array. That's not legal.When an
as const'd array literal appears in a place where a mutable array is expected, it's allowed to subtype back to the mutable version of the array, since it can be seen that no one else has a reference to the array that would become unsound after the array is mutated.medikoo commentedon Aug 8, 2024
Indeed, I missed that in the above reasoning. Technically
testassumes rights to mutate the input, therefore readonly input should be rejected.This is an actual source of confusion. I think the fact that the given array is not referenced anywhere else shouldn't dismiss that it was declared
as const.For example, the following code passes perfectly:
What's interesting is that it will fail for
["test"] as read-only string[].Why is mutation on["test"] as constallowed conditionally?RyanCavanaugh commentedon Aug 8, 2024
Why? That breaks code like this:
See above example - a "fresh"
constarray literal can be safely treated as mutablemedikoo commentedon Aug 8, 2024
Ryan Cavanaugh (@RyanCavanaugh) Why exactly will this break? I'm not sure if I follow this example.
Doesn't
["one", "two"] as constsatisfyreadonly T[]on its own (without downgrading to mutable)?I see that
footakes a readonly array of elements of typeTand returns a mutable array of elements of typeT. I believe there's no assumption that the input and output are the same objects; the only assumption is that they are of the same type. Right?RyanCavanaugh commentedon Aug 9, 2024
Sorry, I meant this:
typescript-bot commentedon Aug 12, 2024
This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
medikoo commentedon Aug 12, 2024
Ryan Cavanaugh (@RyanCavanaugh) I'm not sure if it makes any real difference. What do I miss?
Why exactly there's a room for any error at
p2case?I read it as:
fooreceives the tuple of literal types"one"and"two", therefore the expected output structure is("one" | "two")[].Similarly as if I'd pass
[1, 'string'], the expected output structure would be(number | string)[].I'm not sure how it explains that in previous example we allow mutation for
["test"] as constinput?? That's interesting. So if I do not respond to the comment on the issue during the weekend the issue will be autoclosed? :) Where's the work/life balance thing? ;-)
RyanCavanaugh commentedon Aug 12, 2024
You said
The proposed behavior is that
["one", "two"] as constbe inferred as a readonly array, but it's illegal to pass a readonly array tofoomedikoo commentedon Aug 12, 2024
Thanks for responding. I see now that mutability is tied to type declarations. In this case, the input was declared as
T[], indicating a mutable array of type T. That part wasn’t entirely clear to me at first.I guess
("one" | "two")[]could work in that case(?), but I understand that it would be both inconvenient and error-prone to list all possible items by hand.Therefore, if there's no convenient syntax to pass this information (as mutable), I assume TypeScript intentionally allows a hack where the user can pass an immutable that can be subtyped into a mutable.