Skip to content

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

Merged
merged 15 commits into from
Feb 1, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -17694,14 +17694,28 @@ 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constains -> contains

// 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 mapType(contextualType, t => t.flags & TypeFlags.Instantiable ? instantiateType(t, returnMapper) : t);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mapType call should probably have it's third argument set since we're in contextual typing and preserving string | "x"'s literaliness is desirable.

Copy link
Member

Choose a reason for hiding this comment

The 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 - instantiateType is only going to instantiate instantiable types anyway.

Copy link
Collaborator

@jack-williams jack-williams Jan 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you change the mapType I think you can go ahead and close #29174.

(or ping me and I’ll close it)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weswigham Agreed, we should make sure we use UnionReduction.None. The reason we don't just call instantiateType is that getContextualMapper isn't a super cheap operation and also that we don't want to instantiate contained object types as it is unnecessary and can be expensive.

@jack-williams I'll fix it so we don't need #29174.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -19847,6 +19861,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);
}
}

Expand Down Expand Up @@ -22945,7 +22962,9 @@ 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);
const result = maybeTypeOfKind(type, TypeFlags.Literal) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to strip literal freshness here? Minimally that'd deserve a comment, IMO.

getRegularTypeOfLiteralType(type) : type;
context.contextualType = saveContextualType;
context.contextualMapper = saveContextualMapper;
return result;
Expand Down Expand Up @@ -23022,12 +23041,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 {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4364,6 +4364,7 @@ namespace ts {
inferences: InferenceInfo[]; // Inferences made for each type parameter
flags: InferenceFlags; // Inference flags
compareTypes: TypeComparer; // Type comparer function
returnMapper?: TypeMapper; // Type mapper for inferences from return types (if any)
}

/* @internal */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Type '{ fooProp: string; } & Bar' is not assignable to type 'FooBar'.
tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Type '{ fooProp: "frizzlebizzle"; } & Bar' is not assignable to type 'FooBar'.
Types of property 'fooProp' are incompatible.
Type 'string' is not assignable to type '"hello" | "world"'.
Type '"frizzlebizzle"' is not assignable to type '"hello" | "world"'.


==== tests/cases/compiler/errorMessagesIntersectionTypes02.ts (1 errors) ====
Expand All @@ -19,8 +19,8 @@ tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Ty

let fooBar: FooBar = mixBar({
~~~~~~
!!! error TS2322: Type '{ fooProp: string; } & Bar' is not assignable to type 'FooBar'.
!!! error TS2322: Type '{ fooProp: "frizzlebizzle"; } & Bar' is not assignable to type 'FooBar'.
!!! error TS2322: Types of property 'fooProp' are incompatible.
!!! error TS2322: Type 'string' is not assignable to type '"hello" | "world"'.
!!! error TS2322: Type '"frizzlebizzle"' is not assignable to type '"hello" | "world"'.
fooProp: "frizzlebizzle"
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ declare function mixBar<T>(obj: T): T & Bar;

let fooBar: FooBar = mixBar({
>fooBar : FooBar
>mixBar({ fooProp: "frizzlebizzle"}) : { fooProp: string; } & Bar
>mixBar({ fooProp: "frizzlebizzle"}) : { fooProp: "frizzlebizzle"; } & Bar
>mixBar : <T>(obj: T) => T & Bar
>{ fooProp: "frizzlebizzle"} : { fooProp: string; }
>{ fooProp: "frizzlebizzle"} : { fooProp: "frizzlebizzle"; }

fooProp: "frizzlebizzle"
>fooProp : string
>fooProp : "frizzlebizzle"
>"frizzlebizzle" : "frizzlebizzle"

});
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",
}
]);
};

Loading