From 966a5a81fc64debbd3e37e4b059750de8236258c Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 30 Apr 2023 08:21:23 -0700 Subject: [PATCH 1/3] Add fallback when both co- and contra-variant inferences exist --- src/compiler/checker.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 406ff8715d124..2e45bf526a282 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -25154,22 +25154,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const inference = context.inferences[index]; if (!inference.inferredType) { let inferredType: Type | undefined; - const signature = context.signature; - if (signature) { - const inferredCovariantType = inference.candidates ? getCovariantInference(inference, signature) : undefined; - if (inference.contraCandidates) { - // If we have both co- and contra-variant inferences, we use the co-variant inference if it is not 'never', - // it is a subtype of some contra-variant inference, and no other type parameter is constrained to this type - // parameter and has inferences that would conflict. Otherwise, we use the contra-variant inference. - const useCovariantType = inferredCovariantType && !(inferredCovariantType.flags & TypeFlags.Never) && + let fallbackType: Type | undefined; + if (context.signature) { + const inferredCovariantType = inference.candidates ? getCovariantInference(inference, context.signature) : undefined; + const inferredContravariantType = inference.contraCandidates ? getContravariantInference(inference) : undefined; + if (inferredCovariantType || inferredContravariantType) { + // If we have both co- and contra-variant inferences, we prefer the co-variant inference if it is not 'never', + // all co-variant inferences are subtypes of it (i.e. it isn't one of a conflicting set of candidates), it is + // a subtype of some contra-variant inference, and no other type parameter is constrained to this type parameter + // and has inferences that would conflict. Otherwise, we prefer the contra-variant inference. + const preferCovariantType = inferredCovariantType && (!inferredContravariantType || + !(inferredCovariantType.flags & TypeFlags.Never) && + every(inference.candidates, t => isTypeSubtypeOf(t, inferredCovariantType)) && some(inference.contraCandidates, t => isTypeSubtypeOf(inferredCovariantType, t)) && - every(context.inferences, other => - other !== inference && getConstraintOfTypeParameter(other.typeParameter) !== inference.typeParameter || - every(other.candidates, t => isTypeSubtypeOf(t, inferredCovariantType))); - inferredType = useCovariantType ? inferredCovariantType : getContravariantInference(inference); - } - else if (inferredCovariantType) { - inferredType = inferredCovariantType; + every(context.inferences, other => other === inference || + getConstraintOfTypeParameter(other.typeParameter) !== inference.typeParameter || + every(other.candidates, t => isTypeSubtypeOf(t, inferredCovariantType)))); + inferredType = preferCovariantType ? inferredCovariantType : inferredContravariantType; + fallbackType = preferCovariantType ? inferredContravariantType : inferredCovariantType; } else if (context.flags & InferenceFlags.NoDefault) { // We use silentNeverType as the wildcard that signals no inferences. @@ -25199,7 +25201,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (constraint) { const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper); if (!inferredType || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { - inference.inferredType = inferredType = instantiatedConstraint; + // If the fallback type satisfies the constraint, we pick it. Otherwise, we pick the constraint. + inference.inferredType = fallbackType && context.compareTypes(fallbackType, getTypeWithThisArgument(instantiatedConstraint, fallbackType)) ? fallbackType : instantiatedConstraint; } } } From f0f477ecf3b4d9c639f0b340f8411ad7cc12ec8f Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 30 Apr 2023 08:25:08 -0700 Subject: [PATCH 2/3] Add regression test --- .../coAndContraVariantInferences5.symbols | 66 +++++++++++++++++++ .../coAndContraVariantInferences5.types | 53 +++++++++++++++ .../compiler/coAndContraVariantInferences5.ts | 25 +++++++ 3 files changed, 144 insertions(+) create mode 100644 tests/baselines/reference/coAndContraVariantInferences5.symbols create mode 100644 tests/baselines/reference/coAndContraVariantInferences5.types create mode 100644 tests/cases/compiler/coAndContraVariantInferences5.ts diff --git a/tests/baselines/reference/coAndContraVariantInferences5.symbols b/tests/baselines/reference/coAndContraVariantInferences5.symbols new file mode 100644 index 0000000000000..b9f6b6fc3081d --- /dev/null +++ b/tests/baselines/reference/coAndContraVariantInferences5.symbols @@ -0,0 +1,66 @@ +=== tests/cases/compiler/coAndContraVariantInferences5.ts === +type Thing = 'a' | 'b'; +>Thing : Symbol(Thing, Decl(coAndContraVariantInferences5.ts, 0, 0)) + +function f( +>f : Symbol(f, Decl(coAndContraVariantInferences5.ts, 0, 23)) + + options: SelectOptions, +>options : Symbol(options, Decl(coAndContraVariantInferences5.ts, 2, 11)) +>SelectOptions : Symbol(SelectOptions, Decl(coAndContraVariantInferences5.ts, 17, 2)) +>Thing : Symbol(Thing, Decl(coAndContraVariantInferences5.ts, 0, 0)) + + onChange: (status: Thing | null) => void, +>onChange : Symbol(onChange, Decl(coAndContraVariantInferences5.ts, 3, 34)) +>status : Symbol(status, Decl(coAndContraVariantInferences5.ts, 4, 15)) +>Thing : Symbol(Thing, Decl(coAndContraVariantInferences5.ts, 0, 0)) + +): void { + select({ +>select : Symbol(select, Decl(coAndContraVariantInferences5.ts, 10, 1)) + + options, +>options : Symbol(options, Decl(coAndContraVariantInferences5.ts, 6, 12)) + + onChange, +>onChange : Symbol(onChange, Decl(coAndContraVariantInferences5.ts, 7, 16)) + + }); +} + +declare function select(props: SelectProps): void; +>select : Symbol(select, Decl(coAndContraVariantInferences5.ts, 10, 1)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 12, 24)) +>props : Symbol(props, Decl(coAndContraVariantInferences5.ts, 12, 45)) +>SelectProps : Symbol(SelectProps, Decl(coAndContraVariantInferences5.ts, 12, 77)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 12, 24)) + +type SelectProps = { +>SelectProps : Symbol(SelectProps, Decl(coAndContraVariantInferences5.ts, 12, 77)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 14, 17)) + + options?: SelectOptions; +>options : Symbol(options, Decl(coAndContraVariantInferences5.ts, 14, 41)) +>SelectOptions : Symbol(SelectOptions, Decl(coAndContraVariantInferences5.ts, 17, 2)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 14, 17)) + + onChange: (key: KeyT) => void; +>onChange : Symbol(onChange, Decl(coAndContraVariantInferences5.ts, 15, 34)) +>key : Symbol(key, Decl(coAndContraVariantInferences5.ts, 16, 15)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 14, 17)) + +}; + +type SelectOptions = +>SelectOptions : Symbol(SelectOptions, Decl(coAndContraVariantInferences5.ts, 17, 2)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 19, 19)) + + | Array<{key: KeyT}> +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>key : Symbol(key, Decl(coAndContraVariantInferences5.ts, 20, 13)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 19, 19)) + + | Array; +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) +>KeyT : Symbol(KeyT, Decl(coAndContraVariantInferences5.ts, 19, 19)) + diff --git a/tests/baselines/reference/coAndContraVariantInferences5.types b/tests/baselines/reference/coAndContraVariantInferences5.types new file mode 100644 index 0000000000000..4bcacf5535e32 --- /dev/null +++ b/tests/baselines/reference/coAndContraVariantInferences5.types @@ -0,0 +1,53 @@ +=== tests/cases/compiler/coAndContraVariantInferences5.ts === +type Thing = 'a' | 'b'; +>Thing : "a" | "b" + +function f( +>f : (options: SelectOptions, onChange: (status: Thing | null) => void) => void + + options: SelectOptions, +>options : SelectOptions + + onChange: (status: Thing | null) => void, +>onChange : (status: Thing | null) => void +>status : Thing | null + +): void { + select({ +>select({ options, onChange, }) : void +>select : (props: SelectProps) => void +>{ options, onChange, } : { options: SelectOptions; onChange: (status: Thing | null) => void; } + + options, +>options : SelectOptions + + onChange, +>onChange : (status: Thing | null) => void + + }); +} + +declare function select(props: SelectProps): void; +>select : (props: SelectProps) => void +>props : SelectProps + +type SelectProps = { +>SelectProps : SelectProps + + options?: SelectOptions; +>options : SelectOptions | undefined + + onChange: (key: KeyT) => void; +>onChange : (key: KeyT) => void +>key : KeyT + +}; + +type SelectOptions = +>SelectOptions : SelectOptions + + | Array<{key: KeyT}> +>key : KeyT + + | Array; + diff --git a/tests/cases/compiler/coAndContraVariantInferences5.ts b/tests/cases/compiler/coAndContraVariantInferences5.ts new file mode 100644 index 0000000000000..a569065ea146e --- /dev/null +++ b/tests/cases/compiler/coAndContraVariantInferences5.ts @@ -0,0 +1,25 @@ +// @strict: true +// @noEmit: true + +type Thing = 'a' | 'b'; + +function f( + options: SelectOptions, + onChange: (status: Thing | null) => void, +): void { + select({ + options, + onChange, + }); +} + +declare function select(props: SelectProps): void; + +type SelectProps = { + options?: SelectOptions; + onChange: (key: KeyT) => void; +}; + +type SelectOptions = + | Array<{key: KeyT}> + | Array; From c377c44c985b2eca7d4bae3bea446113bc4dba24 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 1 May 2023 18:15:06 -0700 Subject: [PATCH 3/3] Restore code from #52180 --- src/compiler/checker.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2e45bf526a282..da695c1f391a0 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -25165,10 +25165,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // and has inferences that would conflict. Otherwise, we prefer the contra-variant inference. const preferCovariantType = inferredCovariantType && (!inferredContravariantType || !(inferredCovariantType.flags & TypeFlags.Never) && - every(inference.candidates, t => isTypeSubtypeOf(t, inferredCovariantType)) && some(inference.contraCandidates, t => isTypeSubtypeOf(inferredCovariantType, t)) && - every(context.inferences, other => other === inference || - getConstraintOfTypeParameter(other.typeParameter) !== inference.typeParameter || + every(context.inferences, other => + other !== inference && getConstraintOfTypeParameter(other.typeParameter) !== inference.typeParameter || every(other.candidates, t => isTypeSubtypeOf(t, inferredCovariantType)))); inferredType = preferCovariantType ? inferredCovariantType : inferredContravariantType; fallbackType = preferCovariantType ? inferredContravariantType : inferredCovariantType;