Skip to content

[release-2.8] Combine keyof T inferences (#22525) #22708

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 1 commit into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
49 changes: 30 additions & 19 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11737,7 +11737,10 @@ namespace ts {
else if ((isLiteralType(source) || source.flags & TypeFlags.String) && target.flags & TypeFlags.Index) {
const empty = createEmptyObjectTypeFromStringLiteral(source);
contravariant = !contravariant;
const savePriority = priority;
priority |= InferencePriority.LiteralKeyof;
inferFromTypes(empty, (target as IndexType).type);
priority = savePriority;
contravariant = !contravariant;
}
else if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
Expand Down Expand Up @@ -11980,39 +11983,47 @@ namespace ts {
return candidates;
}

function getContravariantInference(inference: InferenceInfo) {
return inference.priority & InferencePriority.PriorityImpliesCombination ? getIntersectionType(inference.contraCandidates) : getCommonSubtype(inference.contraCandidates);
}

function getCovariantInference(inference: InferenceInfo, context: InferenceContext, signature: Signature) {
// Extract all object literal types and replace them with a single widened and normalized type.
const candidates = widenObjectLiteralCandidates(inference.candidates);
// We widen inferred literal types if
// all inferences were made to top-level occurrences of the type parameter, and
// the type parameter has no constraint or its constraint includes no primitive or literal types, and
// the type parameter was fixed during inference or does not occur at top-level in the return type.
const widenLiteralTypes = inference.topLevel &&
!hasPrimitiveConstraint(inference.typeParameter) &&
(inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter));
const baseCandidates = widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : candidates;
// If all inferences were made from contravariant positions, infer a common subtype. Otherwise, if
// union types were requested or if all inferences were made from the return type position, infer a
// union type. Otherwise, infer a common supertype.
const unwidenedType = context.flags & InferenceFlags.InferUnionTypes || inference.priority & InferencePriority.PriorityImpliesCombination ?
getUnionType(baseCandidates, UnionReduction.Subtype) :
getCommonSupertype(baseCandidates);
return getWidenedType(unwidenedType);
}

function getInferredType(context: InferenceContext, index: number): Type {
const inference = context.inferences[index];
let inferredType = inference.inferredType;
if (!inferredType) {
const signature = context.signature;
if (signature) {
if (inference.candidates) {
// Extract all object literal types and replace them with a single widened and normalized type.
const candidates = widenObjectLiteralCandidates(inference.candidates);
// We widen inferred literal types if
// all inferences were made to top-level ocurrences of the type parameter, and
// the type parameter has no constraint or its constraint includes no primitive or literal types, and
// the type parameter was fixed during inference or does not occur at top-level in the return type.
const widenLiteralTypes = inference.topLevel &&
!hasPrimitiveConstraint(inference.typeParameter) &&
(inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter));
const baseCandidates = widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : candidates;
// If all inferences were made from contravariant positions, infer a common subtype. Otherwise, if
// union types were requested or if all inferences were made from the return type position, infer a
// union type. Otherwise, infer a common supertype.
const unwidenedType = context.flags & InferenceFlags.InferUnionTypes || inference.priority & InferencePriority.PriorityImpliesUnion ?
getUnionType(baseCandidates, UnionReduction.Subtype) :
getCommonSupertype(baseCandidates);
inferredType = getWidenedType(unwidenedType);
inferredType = getCovariantInference(inference, context, signature);
// If we have inferred 'never' but have contravariant candidates. To get a more specific type we
// infer from the contravariant candidates instead.
if (inferredType.flags & TypeFlags.Never && inference.contraCandidates) {
inferredType = getCommonSubtype(inference.contraCandidates);
inferredType = getContravariantInference(inference);
}
}
else if (inference.contraCandidates) {
// We only have contravariant inferences, infer the best common subtype of those
inferredType = getCommonSubtype(inference.contraCandidates);
inferredType = getContravariantInference(inference);
}
else if (context.flags & InferenceFlags.NoDefault) {
// We use silentNeverType as the wildcard that signals no inferences.
Expand Down
7 changes: 4 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3920,10 +3920,11 @@ namespace ts {
HomomorphicMappedType = 1 << 1, // Reverse inference for homomorphic mapped type
MappedTypeConstraint = 1 << 2, // Reverse inference for mapped type
ReturnType = 1 << 3, // Inference made from return type of generic function
NoConstraints = 1 << 4, // Don't infer from constraints of instantiable types
AlwaysStrict = 1 << 5, // Always use strict rules for contravariant inferences
LiteralKeyof = 1 << 4, // Inference made from a string literal to a keyof T
NoConstraints = 1 << 5, // Don't infer from constraints of instantiable types
AlwaysStrict = 1 << 6, // Always use strict rules for contravariant inferences

PriorityImpliesUnion = ReturnType | MappedTypeConstraint, // These priorities imply that the resulting type should be a union of all candidates
PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
}

/* @internal */
Expand Down
7 changes: 4 additions & 3 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2240,9 +2240,10 @@ declare namespace ts {
HomomorphicMappedType = 2,
MappedTypeConstraint = 4,
ReturnType = 8,
NoConstraints = 16,
AlwaysStrict = 32,
PriorityImpliesUnion = 12,
LiteralKeyof = 16,
NoConstraints = 32,
AlwaysStrict = 64,
PriorityImpliesCombination = 28,
}
interface JsFileExtensionInfo {
extension: string;
Expand Down
7 changes: 4 additions & 3 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2240,9 +2240,10 @@ declare namespace ts {
HomomorphicMappedType = 2,
MappedTypeConstraint = 4,
ReturnType = 8,
NoConstraints = 16,
AlwaysStrict = 32,
PriorityImpliesUnion = 12,
LiteralKeyof = 16,
NoConstraints = 32,
AlwaysStrict = 64,
PriorityImpliesCombination = 28,
}
interface JsFileExtensionInfo {
extension: string;
Expand Down
17 changes: 17 additions & 0 deletions tests/baselines/reference/keyofInferenceIntersectsResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//// [keyofInferenceIntersectsResults.ts]
interface X {
a: string;
b: string;
}

declare function foo<T = X>(x: keyof T, y: keyof T): T;
declare function bar<T>(x: keyof T, y: keyof T): T;

const a = foo<X>('a', 'b'); // compiles cleanly
const b = foo('a', 'b'); // also clean
const c = bar('a', 'b'); // still clean

//// [keyofInferenceIntersectsResults.js]
var a = foo('a', 'b'); // compiles cleanly
var b = foo('a', 'b'); // also clean
var c = bar('a', 'b'); // still clean
43 changes: 43 additions & 0 deletions tests/baselines/reference/keyofInferenceIntersectsResults.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
=== tests/cases/conformance/types/typeRelationships/typeInference/keyofInferenceIntersectsResults.ts ===
interface X {
>X : Symbol(X, Decl(keyofInferenceIntersectsResults.ts, 0, 0))

a: string;
>a : Symbol(X.a, Decl(keyofInferenceIntersectsResults.ts, 0, 13))

b: string;
>b : Symbol(X.b, Decl(keyofInferenceIntersectsResults.ts, 1, 14))
}

declare function foo<T = X>(x: keyof T, y: keyof T): T;
>foo : Symbol(foo, Decl(keyofInferenceIntersectsResults.ts, 3, 1))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))
>X : Symbol(X, Decl(keyofInferenceIntersectsResults.ts, 0, 0))
>x : Symbol(x, Decl(keyofInferenceIntersectsResults.ts, 5, 28))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))
>y : Symbol(y, Decl(keyofInferenceIntersectsResults.ts, 5, 39))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 5, 21))

declare function bar<T>(x: keyof T, y: keyof T): T;
>bar : Symbol(bar, Decl(keyofInferenceIntersectsResults.ts, 5, 55))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))
>x : Symbol(x, Decl(keyofInferenceIntersectsResults.ts, 6, 24))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))
>y : Symbol(y, Decl(keyofInferenceIntersectsResults.ts, 6, 35))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))
>T : Symbol(T, Decl(keyofInferenceIntersectsResults.ts, 6, 21))

const a = foo<X>('a', 'b'); // compiles cleanly
>a : Symbol(a, Decl(keyofInferenceIntersectsResults.ts, 8, 5))
>foo : Symbol(foo, Decl(keyofInferenceIntersectsResults.ts, 3, 1))
>X : Symbol(X, Decl(keyofInferenceIntersectsResults.ts, 0, 0))

const b = foo('a', 'b'); // also clean
>b : Symbol(b, Decl(keyofInferenceIntersectsResults.ts, 9, 5))
>foo : Symbol(foo, Decl(keyofInferenceIntersectsResults.ts, 3, 1))

const c = bar('a', 'b'); // still clean
>c : Symbol(c, Decl(keyofInferenceIntersectsResults.ts, 10, 5))
>bar : Symbol(bar, Decl(keyofInferenceIntersectsResults.ts, 5, 55))

52 changes: 52 additions & 0 deletions tests/baselines/reference/keyofInferenceIntersectsResults.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
=== tests/cases/conformance/types/typeRelationships/typeInference/keyofInferenceIntersectsResults.ts ===
interface X {
>X : X

a: string;
>a : string

b: string;
>b : string
}

declare function foo<T = X>(x: keyof T, y: keyof T): T;
>foo : <T = X>(x: keyof T, y: keyof T) => T
>T : T
>X : X
>x : keyof T
>T : T
>y : keyof T
>T : T
>T : T

declare function bar<T>(x: keyof T, y: keyof T): T;
>bar : <T>(x: keyof T, y: keyof T) => T
>T : T
>x : keyof T
>T : T
>y : keyof T
>T : T
>T : T

const a = foo<X>('a', 'b'); // compiles cleanly
>a : X
>foo<X>('a', 'b') : X
>foo : <T = X>(x: keyof T, y: keyof T) => T
>X : X
>'a' : "a"
>'b' : "b"

const b = foo('a', 'b'); // also clean
>b : { a: any; } & { b: any; }
>foo('a', 'b') : { a: any; } & { b: any; }
>foo : <T = X>(x: keyof T, y: keyof T) => T
>'a' : "a"
>'b' : "b"

const c = bar('a', 'b'); // still clean
>c : { a: any; } & { b: any; }
>bar('a', 'b') : { a: any; } & { b: any; }
>bar : <T>(x: keyof T, y: keyof T) => T
>'a' : "a"
>'b' : "b"

57 changes: 57 additions & 0 deletions tests/baselines/reference/keyofInferenceLowerPriorityThanReturn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//// [keyofInferenceLowerPriorityThanReturn.ts]
// #22736
declare class Write {
protected dummy: Write;
}

declare class Col<s, a> {
protected dummy: [Col<s, a>, s, a];
}

declare class Table<Req, Def> {
protected dummy: [Table<Req, Def>, Req, Def];
}

type MakeTable<T1 extends object, T2 extends object> = {
[P in keyof T1]: Col<Write, T1[P]>;
} & {
[P in keyof T2]: Col<Write, T2[P]>;
};

declare class ConflictTarget<Cols> {
public static tableColumns<Cols>(cols: (keyof Cols)[]): ConflictTarget<Cols>;
protected dummy: [ConflictTarget<Cols>, Cols];
}



const bookTable: Table<BookReq, BookDef> = null as any

interface BookReq {
readonly title: string;
readonly serial: number;
}

interface BookDef {
readonly author: string;
readonly numPages: number | null;
}


function insertOnConflictDoNothing<Req extends object, Def extends object>(_table: Table<Req, Def>, _conflictTarget: ConflictTarget<Req & Def>): boolean {
throw new Error();
}

function f() {
insertOnConflictDoNothing(bookTable, ConflictTarget.tableColumns(["serial"])); // <-- No error here; should use the type inferred for the return type of `tableColumns`
}


//// [keyofInferenceLowerPriorityThanReturn.js]
var bookTable = null;
function insertOnConflictDoNothing(_table, _conflictTarget) {
throw new Error();
}
function f() {
insertOnConflictDoNothing(bookTable, ConflictTarget.tableColumns(["serial"])); // <-- No error here; should use the type inferred for the return type of `tableColumns`
}
Loading