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
exactOptionsPropertyTypes - anomolous behavior with Tuples, options, and spreading. #45764
Spreading of Variadic Tuple types, as well as the ability to indicate final position optional parameters with labels, were introduced in ts 4.0. The noted behavior has been been present since then.
However, although there was hope it would be fixed with 4.4's exact exactOptionsParameters, it was not.
typeT1=[x:number,x?:boolean]constt11:T1=[1,undefined]// EXCELLENT!! with exactOptionalPropertyTypes:true this became is an error. constt12:T1=[1,true]constt13:T1=[1]typeT2=['^',...T1,'$']constt21:T2=['^',1,true,'$'];constt22:T2=['^',1,undefined,'$'];// noerr - CLAIM - this SHOULD be an errorconstt23:T2=['^',1,'$'];// err - CLAIM - this SHOULD NOT be an error
🙁 Actual behavior
In the above code, variable declaration for t22 is NOT an err.
In the above code, variable declaration for t23 IS an err.
🙂 Expected behavior
In the above code, variable declaration for t22 IS an err.
In the above code, variable declaration for t23 is NOT an err.
Even though with exactOptionalPropertyTypes , the T1 now no longer considers [number,undefined] to be a legal assignment, when T1 is spread into T2 the old behavior is used instead of the new behavior.
In TypeScript 4.4, the new flag --exactOptionalPropertyTypes specifies that optional property types should be interpreted exactly as written, meaning that | undefined is not added to the type:
Despite the name, it apparently applies to Tuples as well. From the link given in the 4.4 documentation to the related pull
In --strictOptionalProperties mode, optional elements in tuple types are strictly checked when they don't explicitly include
undefined in their type. For example:
// Compile in --strictOptionalProperties mode
function f4(t: [string?]) {
let x = t[0]; // string | undefined
t[0] = 'hello';
t[0] = undefined; // Error, 'undefined' not assignable to 'string'
}
It appears the name changed from strictOptionalProperties to exactOptionalPropertyTypes. (There never was a strictOptionalParameters.)
changed the title [-]exactOptionsParameters - still forcibly inserting undefined with optiona into Tuple when using spread operator on sub Tuple[/-][+]exactOptionsPropertyTypes - a number of anomolies detected with Tuples, options, and spreading.[/+]on Sep 7, 2021
changed the title [-]exactOptionsPropertyTypes - a number of anomolies detected with Tuples, options, and spreading.[/-][+]exactOptionsPropertyTypes - anonolous behavior with Tuples, options, and spreading.[/+]on Sep 7, 2021
changed the title [-]exactOptionsPropertyTypes - anonolous behavior with Tuples, options, and spreading.[/-][+]exactOptionsPropertyTypes - anomolous behavior with Tuples, options, and spreading.[/+]on Sep 8, 2021
FWIW, that JSON dump of type info is not from our compiler API. I’ve never heard of properties like discTypeFlag and ctypes. It would be sort of helpful to me if this issue were focused on the tuple thing and the issue about what I assume is someone’s 3rd party compiler API were relocated somewhere else.
Andrew Branch (@andrewbranch) - Sure - I'll take it out. Should I remove that whole final section "Tangent topic yet critical - Type API may have changed - is it still possible to determine behavior from exposed Type API ?"
It's not third party API or a compiler. It's from code I wrote displaying the information obtained via getTypeOfSymbolAtLocation and getDeclaredTypeOfSymbol, which are part of the (bleeding edge) typescript compiler API. That format you see is that information slightly massaged to take up less space, without losing anything critical. There is a verbose mode which outputs raw-ish information.
(... Tangential information describing motivation for using compiler API's getTypeOfSymbolAtLocation and getDeclaredTypeOfSymbol removed ... )
That final section of my post is about that compiler API - perhaps - no longer enough info to predict what will pass the compiler in some instances (optional vs. explicit undefined).
Andrew Branch (@andrewbranch) - Here is a link to the rawish output. The discTypeFlag is still there but it is just taken from ts.Type['flags']. Sometimes (rarely) more than one flag bit is set, but an unambiguous value is required for discriminating types for processing.
In the out you see some places where r: and c:fields exist side by side. r is for raw. c is the massaged and shortened form, which may be ignored.
Yeah, it would be better if an API request were split into a separate issue please. And it would be easier for me to evaluate without it being intercepted by “a generalized type explorer, which is a component of a typescript Type to schema translator.” If you’re not using the compiler API directly, I can’t tell whether the compiler API is actually deficient.
But I think that is not the Compiler API you mean right now. You mean using the compiler API exposed either through tsc or ts.createProgram, ts.getXXXDiagnostics and program.emit, which is 99.9% of usage. I agree 100%.
No, I’m familiar with the full checker API, and I’m sympathetic to ensuring it’s usable, it just doesn’t seem relevant to this issue and I want to evaluate it without it having been serialized into a different structure with different property names by some mysterious 3rd party tool.
Andrew Branch (@andrewbranch) - You are absolutely correct that it is a separate issue and I think my judgement was poor. (I've already figure how to solve that separate API issue so don't worry).
About the problem at hand I made some more examples around the strange result.
type T1 = [x:number, x?:boolean]
const t11:T1 = [1,undefined] // EXCELLENT!! with exactOptionalPropertyTypes:true this became is an error.
const t12:T1 = [1,true]
const t13:T1 = [1]
type T2a = [...T1];
const t2a1:T2a = [1,true];
const t2a2:T2a = [1,undefined]; // err - ok
const t2a3:T2a = [1]; // noerr - ok
type T2b = ['^',...T1];
const t2b1:T2b = ['^',1,true];
const t2b2:T2b = ['^',1,undefined]; // err - ok
const t2b3:T2b = ['^',1]; // noerr - ok
type T2 = ['^',...T1,'$']
const t21:T2 = ['^',1,true,'$'];
const t22:T2 = ['^',1,undefined,'$']; // noerr - CLAIM - this SHOULD be an error
const t23:T2 = ['^',1,'$']; // err - CLAIM - this SHOULD NOT be an error
type T3 = [x:number, x:boolean]|[x:number]
type T4 = ['^',...T3,'$']
const t41:T4 = ['^',1,true,'$'];
const t42:T4 = ['^',1,undefined,'$']; // err - ok
const t43:T4 = ['^',1,'$']; // pass - ok
type T4eq = ['^',...[x:number, x:boolean],'$'] | ['^',...[x:number],'$']
const t41eq:T4eq = ['^',1,true,'$'];
const t42eq:T4eq = ['^',1,undefined,'$']; // err - ok
const t43eq:T4eq = ['^',1,'$']; // pass - ok
Out of T2a,T2b, T2, only T2 is failing.The difference with T2 is that it has an element after the variadic.
Whether there is an another element before the variadic doesn't seem to make a difference.
The ones that work correctly (T2a,T2b) (and T4 and T4eq) all make unions at the highest level. T4eq is literally a union at the highest level, the others look like that internally.
The latter examples imply that the expected behavior would be to produce ["^", number, boolean?, "$"] but that is an impossible type (A required element cannot follow an optional element.(1257)).
When spreading a tuple without adding extra trailing elements the original optional flag is preserved (and not expanded into T | undefined). So I'm really not sure if there is anything actionable here. I think that the issue could be closed.
Yeah, there are a couple things wrapped up into one here
Should tuples support middle optional elements?
If not, what should a spread do if doing one would produce one?
The first one is effectively a feature request, but not one we'd be likely to pick up since I think it'd have serious combinatorial explosion problems.
Re second one, the current behavior is maybe the best approximation of what we could do. I think there's an argument to be made that under EOPT, tuple spreads should behave as if you had written
typeT2=['^',...Required<T1>,'$'];constt21:T2=['^',1,true,'$'];constt22:T2=['^',1,undefined,'$'];// should be + is an error
Though if you want that behavior, you can write that today, but if you like today's behavior more and we switch, there wouldn't be a way to go back except to turn off EOPT -- but maybe that's what EOPT means, after all.
Having just read what I wrote, I'm maybe unsure now, since it's not like T2 acquires a wrong type here. It's maybe surprising, but the definition doesn't introduce a novel soundness hole or anything.
Activity
MartinJohns commentedon Sep 7, 2021
This is the first time I ever heard of this flag. It's not mentioned in the 4.4 release notes. Where did you find this?
craigphicks commentedon Sep 7, 2021
Martin Johns (@MartinJohns) -
exactOptionsPropertyNames.Despite the name, it apparently applies to Tuples as well. From the link given in the 4.4 documentation to the related pull
It appears the name changed from
strictOptionalPropertiestoexactOptionalPropertyTypes. (There never was astrictOptionalParameters.)[-]exactOptionsParameters - still forcibly inserting undefined with optiona into Tuple when using spread operator on sub Tuple[/-][+]exactOptionsPropertyTypes - a number of anomolies detected with Tuples, options, and spreading.[/+][-]exactOptionsPropertyTypes - a number of anomolies detected with Tuples, options, and spreading.[/-][+]exactOptionsPropertyTypes - anonolous behavior with Tuples, options, and spreading.[/+]craigphicks commentedon Sep 7, 2021
Andrew Branch (@andrewbranch) thank you
[-]exactOptionsPropertyTypes - anonolous behavior with Tuples, options, and spreading.[/-][+]exactOptionsPropertyTypes - anomolous behavior with Tuples, options, and spreading.[/+]andrewbranch commentedon Sep 8, 2021
FWIW, that JSON dump of type info is not from our compiler API. I’ve never heard of properties like
discTypeFlagandctypes. It would be sort of helpful to me if this issue were focused on the tuple thing and the issue about what I assume is someone’s 3rd party compiler API were relocated somewhere else.craigphicks commentedon Sep 8, 2021
Andrew Branch (@andrewbranch) - Sure - I'll take it out. Should I remove that whole final section "Tangent topic yet critical - Type API may have changed - is it still possible to determine behavior from exposed Type API ?"
It's not third party API or a compiler. It's from code I wrote displaying the information obtained via
getTypeOfSymbolAtLocationandgetDeclaredTypeOfSymbol, which are part of the (bleeding edge) typescript compiler API. That format you see is that information slightly massaged to take up less space, without losing anything critical. There is a verbose mode which outputs raw-ish information.(... Tangential information describing motivation for using compiler API's
getTypeOfSymbolAtLocationandgetDeclaredTypeOfSymbolremoved ... )That final section of my post is about that compiler API - perhaps - no longer enough info to predict what will pass the compiler in some instances (optional vs. explicit undefined).
craigphicks commentedon Sep 8, 2021
Andrew Branch (@andrewbranch) - Here is a link to the rawish output. The
discTypeFlagis still there but it is just taken fromts.Type['flags']. Sometimes (rarely) more than one flag bit is set, but an unambiguous value is required for discriminating types for processing.In the out you see some places where
r:and c:fields exist side by side.ris for raw.cis the massaged and shortened form, which may be ignored.andrewbranch commentedon Sep 8, 2021
Yeah, it would be better if an API request were split into a separate issue please. And it would be easier for me to evaluate without it being intercepted by “a generalized type explorer, which is a component of a typescript Type to schema translator.” If you’re not using the compiler API directly, I can’t tell whether the compiler API is actually deficient.
craigphicks commentedon Sep 8, 2021
Andrew Branch (@andrewbranch) Removed.
getTypeOfSymbolAtLocationandgetDeclaredTypeOfSymbolare both member functions of checker, and they are both exposed as part of the compiler API. For example,getTypeOfSymbolAtLocationis introduced on the Typescript github page "Using the Compiler API".But I think that is not the Compiler API you mean right now. You mean using the compiler API exposed either through
tscorts.createProgram,ts.getXXXDiagnosticsandprogram.emit, which is 99.9% of usage. I agree 100%.andrewbranch commentedon Sep 8, 2021
No, I’m familiar with the full checker API, and I’m sympathetic to ensuring it’s usable, it just doesn’t seem relevant to this issue and I want to evaluate it without it having been serialized into a different structure with different property names by some mysterious 3rd party tool.
craigphicks commentedon Sep 9, 2021
Andrew Branch (@andrewbranch) - You are absolutely correct that it is a separate issue and I think my judgement was poor. (I've already figure how to solve that separate API issue so don't worry).
About the problem at hand I made some more examples around the strange result.
Out of T2a,T2b, T2, only T2 is failing.The difference with T2 is that it has an element after the variadic.
Whether there is an another element before the variadic doesn't seem to make a difference.
The ones that work correctly (T2a,T2b) (and T4 and T4eq) all make unions at the highest level.
T4eqis literally a union at the highest level, the others look like that internally.playground
5 remaining items
Andarist commentedon Mar 2, 2023
The OP claims that the type produced by this spread is incorrect:
The latter examples imply that the expected behavior would be to produce
["^", number, boolean?, "$"]but that is an impossible type (A required element cannot follow an optional element.(1257)).When spreading a tuple without adding extra trailing elements the original optional flag is preserved (and not expanded into
T | undefined). So I'm really not sure if there is anything actionable here. I think that the issue could be closed.RyanCavanaugh commentedon Mar 2, 2023
Yeah, there are a couple things wrapped up into one here
The first one is effectively a feature request, but not one we'd be likely to pick up since I think it'd have serious combinatorial explosion problems.
Re second one, the current behavior is maybe the best approximation of what we could do. I think there's an argument to be made that under EOPT, tuple spreads should behave as if you had written
Though if you want that behavior, you can write that today, but if you like today's behavior more and we switch, there wouldn't be a way to go back except to turn off EOPT -- but maybe that's what EOPT means, after all.
RyanCavanaugh commentedon Mar 2, 2023
Having just read what I wrote, I'm maybe unsure now, since it's not like
T2acquires a wrong type here. It's maybe surprising, but the definition doesn't introduce a novel soundness hole or anything.