-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Infer contextual types from generic return types #29478
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
Changes from 7 commits
1b4bce2
da9feca
bb1c91f
4163d2d
c809002
0b366ce
0b1da02
60e7528
dbea08f
cf479fc
2ea0251
7c1bb14
fffd774
c581575
42be36d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14591,18 +14591,18 @@ namespace ts { | |
} | ||
|
||
function inferFromProperties(source: Type, target: Type) { | ||
if (isTupleType(source)) { | ||
if (isArrayType(source) || isTupleType(source)) { | ||
if (isTupleType(target)) { | ||
const sourceLength = getLengthOfTupleType(source); | ||
const sourceLength = isTupleType(source) ? getLengthOfTupleType(source) : 0; | ||
const targetLength = getLengthOfTupleType(target); | ||
const sourceRestType = getRestTypeOfTupleType(source); | ||
const sourceRestType = isTupleType(source) ? getRestTypeOfTupleType(source) : getElementTypeOfArrayType(source); | ||
const targetRestType = getRestTypeOfTupleType(target); | ||
const fixedLength = targetLength < sourceLength || sourceRestType ? targetLength : sourceLength; | ||
for (let i = 0; i < fixedLength; i++) { | ||
inferFromTypes(i < sourceLength ? source.typeArguments![i] : sourceRestType!, target.typeArguments![i]); | ||
inferFromTypes(i < sourceLength ? (<TypeReference>source).typeArguments![i] : sourceRestType!, target.typeArguments![i]); | ||
} | ||
if (targetRestType) { | ||
const types = fixedLength < sourceLength ? source.typeArguments!.slice(fixedLength, sourceLength) : []; | ||
const types = fixedLength < sourceLength ? (<TypeReference>source).typeArguments!.slice(fixedLength, sourceLength) : []; | ||
if (sourceRestType) { | ||
types.push(sourceRestType); | ||
} | ||
|
@@ -17694,19 +17694,49 @@ namespace ts { | |
// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily | ||
// be "pushed" onto a node using the contextualType property. | ||
function getApparentTypeOfContextualType(node: Expression): Type | undefined { | ||
let contextualType = getContextualType(node); | ||
contextualType = contextualType && mapType(contextualType, getApparentType); | ||
if (contextualType && contextualType.flags & TypeFlags.Union) { | ||
if (isObjectLiteralExpression(node)) { | ||
return discriminateContextualTypeByObjectMembers(node, contextualType as UnionType); | ||
const contextualType = instantiateContextualType(getContextualType(node), node); | ||
if (contextualType) { | ||
const apparentType = mapType(contextualType, getApparentType); | ||
if (apparentType.flags & TypeFlags.Union) { | ||
if (isObjectLiteralExpression(node)) { | ||
return discriminateContextualTypeByObjectMembers(node, apparentType as UnionType); | ||
} | ||
else if (isJsxAttributes(node)) { | ||
return discriminateContextualTypeByJSXAttributes(node, apparentType as UnionType); | ||
} | ||
} | ||
else if (isJsxAttributes(node)) { | ||
return discriminateContextualTypeByJSXAttributes(node, contextualType as UnionType); | ||
return apparentType; | ||
} | ||
} | ||
|
||
// If the given contextual type constains instantiable types and if a mapper representing | ||
// return type inferences is available, instantiate those types using that mapper. | ||
function instantiateContextualType(contextualType: Type | undefined, node: Expression): Type | undefined { | ||
if (contextualType && maybeTypeOfKind(contextualType, TypeFlags.Instantiable)) { | ||
const returnMapper = (<InferenceContext>getContextualMapper(node)).returnMapper; | ||
if (returnMapper) { | ||
return instantiateInstantiableTypes(contextualType, returnMapper); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also: I'm wondering why bother with the map type/instantiate flag check at all - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you change the (or ping me and I’ll close it) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @weswigham Agreed, we should make sure we use @jack-williams I'll fix it so we don't need #29174. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ahejlsberg Grand, closing the PR. Might be worth adding the example from #29168 to this PR, just to make sure it’s fixed. |
||
} | ||
} | ||
return contextualType; | ||
} | ||
|
||
// This function is similar to instantiateType, except (a) that it only instantiates types that | ||
// are classified as instantiable (i.e. it doesn't instantiate object types), and (b) it performs | ||
// no reductions on instantiated union types. | ||
function instantiateInstantiableTypes(type: Type, mapper: TypeMapper): Type { | ||
if (type.flags & TypeFlags.Instantiable) { | ||
return instantiateType(type, mapper); | ||
} | ||
if (type.flags & TypeFlags.Union) { | ||
return getUnionType(map((<UnionType>type).types, t => instantiateInstantiableTypes(t, mapper)), UnionReduction.None); | ||
} | ||
if (type.flags & TypeFlags.Intersection) { | ||
return getIntersectionType(map((<IntersectionType>type).types, t => instantiateInstantiableTypes(t, mapper))); | ||
} | ||
return type; | ||
} | ||
|
||
/** | ||
* Woah! Do you really want to use this function? | ||
* | ||
|
@@ -19847,6 +19877,9 @@ namespace ts { | |
const inferenceTargetType = getReturnTypeOfSignature(signature); | ||
// Inferences made from return types have lower priority than all other inferences. | ||
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType); | ||
// Create a type mapper for instantiating generic contextual types using the inferences made | ||
// from the return type. | ||
context.returnMapper = cloneTypeMapper(context); | ||
} | ||
} | ||
|
||
|
@@ -22945,7 +22978,12 @@ namespace ts { | |
context.contextualMapper = contextualMapper; | ||
const checkMode = contextualMapper === identityMapper ? CheckMode.SkipContextSensitive : | ||
contextualMapper ? CheckMode.Inferential : CheckMode.Contextual; | ||
const result = checkExpression(node, checkMode); | ||
const type = checkExpression(node, checkMode); | ||
// We strip literal freshness when an appropriate contextual type is present such that contextually typed | ||
// literals always preserve their literal types (otherwise they might widen during type inference). An alternative | ||
// here would be to not mark contextually typed literals as fresh in the first place. | ||
const result = maybeTypeOfKind(type, TypeFlags.Literal) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ? | ||
getRegularTypeOfLiteralType(type) : type; | ||
context.contextualType = saveContextualType; | ||
context.contextualMapper = saveContextualMapper; | ||
return result; | ||
|
@@ -23022,12 +23060,9 @@ namespace ts { | |
} | ||
|
||
function checkExpressionForMutableLocation(node: Expression, checkMode: CheckMode | undefined, contextualType?: Type, forceTuple?: boolean): Type { | ||
if (arguments.length === 2) { | ||
contextualType = getContextualType(node); | ||
} | ||
const type = checkExpression(node, checkMode, forceTuple); | ||
return isTypeAssertion(node) ? type : | ||
getWidenedLiteralLikeTypeForContextualType(type, contextualType); | ||
getWidenedLiteralLikeTypeForContextualType(type, instantiateContextualType(arguments.length === 2 ? getContextualType(node) : contextualType, node)); | ||
} | ||
|
||
function checkPropertyAssignment(node: PropertyAssignment, checkMode?: CheckMode): Type { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
tests/cases/compiler/inferFromGenericFunctionReturnTypes3.ts(28,30): error TS2345: Argument of type 'string' is not assignable to parameter of type '"bar"'. | ||
|
||
|
||
==== tests/cases/compiler/inferFromGenericFunctionReturnTypes3.ts (1 errors) ==== | ||
// Repros from #5487 | ||
|
||
function truePromise(): Promise<true> { | ||
return Promise.resolve(true); | ||
} | ||
|
||
interface Wrap<T> { | ||
value: T; | ||
} | ||
|
||
function wrap<T>(value: T): Wrap<T> { | ||
return { value }; | ||
} | ||
|
||
function wrappedFoo(): Wrap<'foo'> { | ||
return wrap('foo'); | ||
} | ||
|
||
function wrapBar(value: 'bar'): Wrap<'bar'> { | ||
return { value }; | ||
} | ||
|
||
function wrappedBar(): Wrap<'bar'> { | ||
const value = 'bar'; | ||
const inferred = wrapBar(value); | ||
const literal = wrapBar('bar'); | ||
const value2: string = 'bar'; | ||
const literal2 = wrapBar(value2); // Error | ||
~~~~~~ | ||
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type '"bar"'. | ||
return wrap(value); | ||
} | ||
|
||
function wrappedBaz(): Wrap<'baz'> { | ||
const value: 'baz' = 'baz'; | ||
return wrap(value); | ||
} | ||
|
||
// Repro from #11152 | ||
|
||
interface FolderContentItem { | ||
type: 'folder' | 'file'; | ||
} | ||
|
||
let a: FolderContentItem[] = []; | ||
a = [1, 2, 3, 4, 5].map(v => ({ type: 'folder' })); | ||
|
||
// Repro from #11312 | ||
|
||
let arr: Array<[number, number]> = [[1, 2]] | ||
|
||
let mappedArr: Array<[number, number]> = arr.map(([x, y]) => { | ||
return [x, y]; | ||
}) | ||
|
||
// Repro from #13594 | ||
|
||
export namespace DiagnosticSeverity { | ||
export const Error = 1; | ||
export const Warning = 2; | ||
export const Information = 3; | ||
export const Hint = 4; | ||
} | ||
|
||
export type DiagnosticSeverity = 1 | 2 | 3 | 4; | ||
|
||
export interface Diagnostic { | ||
severity?: DiagnosticSeverity; | ||
code?: number | string; | ||
source?: string; | ||
message: string; | ||
} | ||
|
||
function bug(): Diagnostic[] { | ||
let values: any[] = []; | ||
return values.map((value) => { | ||
return { | ||
severity: DiagnosticSeverity.Error, | ||
message: 'message' | ||
} | ||
}); | ||
} | ||
|
||
// Repro from #22870 | ||
|
||
function objectToMap(obj: any) { | ||
return new Map(Object.keys(obj).map(key => [key, obj[key]])); | ||
}; | ||
|
||
// Repro from #24352 | ||
|
||
interface Person { | ||
phoneNumbers: { | ||
__typename: 'PhoneNumber'; | ||
}[]; | ||
} | ||
|
||
function createPerson(): Person { | ||
return { | ||
phoneNumbers: [1].map(() => ({ | ||
__typename: 'PhoneNumber' | ||
})) | ||
}; | ||
} | ||
|
||
// Repro from #26621 | ||
|
||
type Box<T> = { value: T }; | ||
declare function box<T>(value: T): Box<T>; | ||
|
||
type WinCondition = | ||
| { type: 'win', player: string } | ||
| { type: 'draw' }; | ||
|
||
let zz: Box<WinCondition> = box({ type: 'draw' }); | ||
|
||
type WinType = 'win' | 'draw'; | ||
|
||
let yy: Box<WinType> = box('draw'); | ||
|
||
// Repro from #27074 | ||
|
||
interface OK<T> { | ||
kind: "OK"; | ||
value: T; | ||
} | ||
export function ok<T>(value: T): OK<T> { | ||
return { | ||
kind: "OK", | ||
value: value | ||
}; | ||
} | ||
|
||
let result: OK<[string, number]> = ok(["hello", 12]); | ||
|
||
// Repro from #25889 | ||
|
||
interface I { | ||
code: 'mapped', | ||
name: string, | ||
} | ||
|
||
const a3: I[] = ['a', 'b'].map(name => { | ||
return { | ||
code: 'mapped', | ||
name, | ||
} | ||
}); | ||
|
||
// Repro from https://www.memsql.com/blog/porting-30k-lines-of-code-from-flow-to-typescript/ | ||
|
||
type Player = { | ||
name: string; | ||
age: number; | ||
position: "STRIKER" | "GOALKEEPER", | ||
}; | ||
|
||
type F = () => Promise<Array<Player>>; | ||
|
||
const f1: F = () => { | ||
return Promise.all([ | ||
{ | ||
name: "David Gomes", | ||
age: 23, | ||
position: "GOALKEEPER", | ||
}, { | ||
name: "Cristiano Ronaldo", | ||
age: 33, | ||
position: "STRIKER", | ||
} | ||
]); | ||
}; | ||
|
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.
constains -> contains