-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Adds support for type parameter defaults #13487
Conversation
@RyanCavanaugh: this is my take on refreshing the genericDefaults PR. It should resolve the open items you had on #6354 regarding checking defaults against constraints. I also added missing support for typeToString and the declaration emitter. Please let me know if you can suggest any additional tests. @mhegazy: when we discussed this, I suggested that interface declarations that have defaults must have identical defaults, allowing you to mix declarations with and without defaults: interface X<A, B> {}
interface X<A, B = number> {} However, this PR does not reflect that suggestion and is more strict, requiring all declarations to have identical defaults as well. It seems more consistent and we can loosen this restriction at any point if we so choose. @ahejlsberg: The |
src/compiler/checker.ts
Outdated
const types = (<UnionOrIntersectionType>type).types; | ||
const defaultTypes = filter(map(types, getResolvedDefaultWorker), x => x !== circularConstraintOrDefaultType); | ||
return type.flags & TypeFlags.Union && defaultTypes.length === types.length ? getUnionType(defaultTypes) : | ||
type.flags & TypeFlags.Intersection && defaultTypes.length ? getIntersectionType(defaultTypes) : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little confusing - so if there were any circular types in a union, you return undefined
. But you disregard all circularities in an intersection type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reused this from line 4827, above. It does seem like it should also be performing a comparison against types.length as well, however I am unsure as to whether there was a specific reason for 4827 to ignore circularity in an intersection type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/compiler/checker.ts
Outdated
typeArguments = []; | ||
} | ||
const mapper: TypeMapper = t => { | ||
for (let i = 0; i < numTypeParameters; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You technically don't need to run through the initial type arguments that were already supplied, do you? Could you just start at numTypeArguments
as you did below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, if a type parameter has a default type that is a reference to another type parameter, then we need to map it to the other, possibly earlier, type parameter.
Otherwise you end up with this:
declare function f<T, U = T>();
f<number>() // becomes f<number, T>()
The T
is invalid at the call site, so we replace it with the type number
for the supplied type argument.
src/compiler/checker.ts
Outdated
} | ||
const mapper: TypeMapper = t => { | ||
for (let i = 0; i < numTypeParameters; i++) { | ||
if (t === typeParameters[i]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (t !== typeParameters[i]) {
continue;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have restructured and simplified this in 25cb02e
The latest commit makes the following changes (also noted above):
For example: interface X { } // no type parameters
interface X<T = never> { } // ok, add a new default type parameter
interface Y<T> { } // one required type parameter
interface Y<T = never> { } // ok, make an existing type parameter optional
interface Z { } // no type parameters
interface Z<T> { } // error, type parameter lists aren't identical
//// uncomment the following to remove the error as the declarations will merge:
// interface Z<T = never> { } |
@ahejlsberg Per our conversation in the design meeting, I've further modified |
src/compiler/checker.ts
Outdated
function getInferredType(context: InferenceContext, index: number): Type { | ||
let inferredType = context.inferredTypes[index]; | ||
let inferredType = context.inferredTypes[index] || getSuppliedType(context, index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird newline thing here
src/compiler/checker.ts
Outdated
minTypeArgumentCount = maxTypeArgumentCount; | ||
} | ||
if (typeParameters && typeParameters.length > maxTypeArgumentCount) { | ||
// Trim the type parameters to the maximum length |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's an example of how this could happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to preserve the current behavior with respect to where we report errors for type parameter lists:
interface X<T> {} // no error on first decl
interface X<T, U> {} // no defaults so we trim to `<T>` and report error for this decl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it works to trim like this. You might have members that reference the trimmed type parameter and those members would never be properly instantiated. In other words, the trimmed type parameters could "leak".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent is that excess type parameters would result in an error, consistent with our behavior today as in the example above. Today we only get the type parameters of the first declaration, so this shouldn't be any different than how we would instantiate X
above today.
src/compiler/diagnosticMessages.json
Outdated
@@ -2035,6 +2035,18 @@ | |||
"category": "Error", | |||
"code": 2704 | |||
}, | |||
"Required type parameters may not follow optional type parameters": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing trailing period in this and in line 2046
Following the discussion during the design meeting, I am making the following changes:
For example: declare function f<T, U = T>(a: T, b?: U): void;
f(1); // ok. Using inference: T is number, U is number.
f(1, "a"); // ok. Using inference: T is number, U is string.
f<number>(1, "a"); // error. Not using inference: T is number, U is number. While this is generally stricter, we can loosen this restriction at a later date if necessary. |
I have added the following restriction:
This is to provide an error for the following case: interface A { }
interface B {
x: A; // error
}
interface B<A = number> {
} |
I have looked into including #13609 in this change, but it results in significant change in behavior that may be too much to take on if we want to take this for 2.2. @ahejlsberg Do you have any other feedback for this change? |
@rbuckton You mentioned taking this for 2.2 in an earlier comment but I see that this issue still has the 2.3 milestone attached. Will this be in 2.2? |
Can't wait for this, writing a library in TS and this will save the consumers from a lot of confusion. Great work! |
This PR adds support for defaults for generic type parameters and replaces #6354 as it is fairly out of date.
Default types for generic type parameters have the following syntax:
Default types have the following rules:
{}
will be used.{}
is inferred. This is the existing behvaior.This PR also updates the signature for
Promise<T>
andPromiseLike<T>
to take full advantage of the new support for type parameter defaults.Fixes #2175
Fixes #10977
Fixes #12725
Fixes #12886
Fixes #12910
Fixes #13008
Fixes #13015
Fixes #13117