Skip to content

Commit be90172

Browse files
committed
Strengthen and unify negated type <-> filtering conditional workaround rule
1 parent 0c876d2 commit be90172

11 files changed

Lines changed: 1689 additions & 32 deletions

‎tsc/internal/checker/checker.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ type CachedTypeKey struct {
152152
typeId TypeId
153153
}
154154

155+
type DistributiveInstantiationKey struct {
156+
typeId TypeId
157+
typeParameterId TypeId
158+
}
159+
155160
// NarrowedTypeKey
156161

157162
type NarrowedTypeKey struct {
@@ -651,6 +656,7 @@ type Checker struct {
651656
thisExpandoLocations map[*ast.Symbol]*ast.Node
652657
subtypeReductionCache map[CacheHashKey][]*Type
653658
cachedTypes map[CachedTypeKey]*Type
659+
distributiveInstantiationCache map[DistributiveInstantiationKey]bool
654660
cachedSignatures map[CachedSignatureKey]*Signature
655661
undefinedProperties map[*ast.Symbol]*ast.Symbol
656662
narrowedTypes map[NarrowedTypeKey]*Type
@@ -990,6 +996,7 @@ func NewChecker(program Program, tracer *Tracer) (*Checker, *sync.Mutex) {
990996
c.thisExpandoLocations = make(map[*ast.Symbol]*ast.Node)
991997
c.subtypeReductionCache = make(map[CacheHashKey][]*Type)
992998
c.cachedTypes = make(map[CachedTypeKey]*Type)
999+
c.distributiveInstantiationCache = make(map[DistributiveInstantiationKey]bool)
9931000
c.cachedSignatures = make(map[CachedSignatureKey]*Signature)
9941001
c.undefinedProperties = make(map[*ast.Symbol]*ast.Symbol)
9951002
c.narrowedTypes = make(map[NarrowedTypeKey]*Type)

‎tsc/internal/checker/relater.go‎

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3616,8 +3616,8 @@ func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, repo
36163616
nameType := r.c.getNameTypeFromMappedType(targetType)
36173617
constraintType := r.c.getConstraintTypeFromMappedType(targetType)
36183618
var targetKeys *Type
3619-
isFiltering := nameType != nil && r.c.isMappedTypeWithKeyofConstraintDeclaration(targetType)
3620-
if isFiltering {
3619+
isRemappedKeyof := nameType != nil && r.c.isMappedTypeWithKeyofConstraintDeclaration(targetType)
3620+
if isRemappedKeyof {
36213621
// we need to get the apparent mappings and union them with the generic mappings, since some properties may be
36223622
// missing from the `constraintType` which will otherwise be mapped in the object
36233623
mappedKeys := r.c.getApparentMappedTypeKeys(nameType, targetType)
@@ -3628,15 +3628,14 @@ func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, repo
36283628
} else {
36293629
targetKeys = constraintType
36303630
}
3631-
if isFiltering && source.flags&TypeFlagsIntersection != 0 && r.someTypeRelatedToType(source, constraintType, false, intersectionState) == TernaryTrue {
3632-
// run the filter over `source` - some part of `source` is related to the constraint - if source's other elements end up filtering the same way the nameType does, the assignment can pass
3633-
// This effectively is a workaround for a lack of distributive conditional/distributive homomorphic mapped types relationship rules that uses execution of the distribution (even at higher order)
3634-
// to approximate a better result.
3635-
// See the homomorphic case in tsc\testdata\tests\cases\compiler\indexMappedTypeWithFilteredKeysWithNegation.ts
3636-
sourceInstantiatedTarget := r.c.instantiateType(nameType, newSimpleTypeMapper(r.c.getTypeParameterFromMappedType(targetType), source))
3637-
broadTargetKeys := r.c.getUnionType([]*Type{targetKeys, sourceInstantiatedTarget})
3638-
if r.isRelatedTo(source, broadTargetKeys, RecursionFlagsTarget, false) == TernaryTrue {
3639-
return TernaryTrue
3631+
if isRemappedKeyof && source.flags&TypeFlagsIntersection != 0 {
3632+
if filteredNameType := r.getOptionalSimplificationOfConditional(nameType); filteredNameType != nil && r.someTypeRelatedToType(source, constraintType, false, intersectionState) == TernaryTrue {
3633+
// Apply the filter only to a source already constrained to the mapped keys.
3634+
sourceInstantiatedTarget := r.c.instantiateType(filteredNameType, newSimpleTypeMapper(r.c.getTypeParameterFromMappedType(targetType), source))
3635+
broadTargetKeys := r.c.getUnionType([]*Type{targetKeys, sourceInstantiatedTarget})
3636+
if r.isRelatedTo(source, broadTargetKeys, RecursionFlagsTarget, false) == TernaryTrue {
3637+
return TernaryTrue
3638+
}
36403639
}
36413640
}
36423641
if r.isRelatedTo(source, targetKeys, RecursionFlagsTarget, reportErrors) == TernaryTrue {
@@ -4024,32 +4023,72 @@ func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, repo
40244023
return TernaryFalse
40254024
}
40264025

4026+
// TODO: Replace this heuristic with proper distributive conditional type relationship rules.
4027+
// For a distributive A extends B ? never : Q<A> without infer parameters, rewrite to Q<A & not B>.
4028+
// A definitely false check uses Q<A> unchanged; a definitely true check cannot use the rewrite.
4029+
// Otherwise B must be independent of A, and Q<never> must be never. The conditional already distributes,
4030+
// but we also probe whether Q<X | Y> agrees with Q<X> | Q<Y> before moving the union inside Q.
4031+
// See Repeated in indexMappedTypeWithFilteredKeysWithNegationErrors.ts, where moving the union
4032+
// inside `${A}-${A}` would lose the correlation between occurrences of A.
40274033
func (r *Relater) getOptionalSimplificationOfConditional(target *Type) *Type {
40284034
if target.flags&TypeFlagsConditional == 0 {
40294035
return nil
40304036
}
4031-
// TODO: This is a kludge for not having proper rules for relating distributive conditional types.
4032-
// We rewrite `A extends B ? never : Q<A>` into `Q<A & not B>` - provided that substitution
4033-
// actually changes it. This is not *particularly* accurate, but enables the common case of relating a filtering mapped type to the equivalent filtering negated type.
4034-
// See the non-homomorphic case in tsc\testdata\tests\cases\compiler\indexMappedTypeWithFilteredKeysWithNegation.ts for an example.
4035-
root := target.AsConditionalType().root
4036-
if root.isDistributive && r.c.getTrueTypeFromConditionalType(target).flags&TypeFlagsNever != 0 {
4037-
f := r.c.getFalseTypeFromConditionalType(target)
4038-
remappedFalseType := r.c.instantiateType(
4039-
r.c.getTypeFromTypeNode(root.node.FalseType),
4040-
r.c.combineTypeMappers(
4041-
newSimpleTypeMapper(
4042-
getNonDistributedTypeParameter(root.checkType),
4043-
r.c.getIntersectionType([]*Type{root.checkType, r.c.getNegatedType(root.extendsType)}),
4044-
),
4045-
target.AsConditionalType().mapper,
4046-
),
4047-
)
4048-
if remappedFalseType != f {
4049-
return remappedFalseType
4050-
}
4037+
conditional := target.AsConditionalType()
4038+
root := conditional.root
4039+
if !root.isDistributive || len(root.inferTypeParameters) != 0 || r.c.getTrueTypeFromConditionalType(target).flags&TypeFlagsNever == 0 {
4040+
return nil
40514041
}
4052-
return nil
4042+
permissiveExtendsType := r.c.getPermissiveInstantiation(conditional.extendsType)
4043+
skipTrue := !someType(r.c.getPermissiveInstantiation(conditional.checkType), func(t *Type) bool {
4044+
return r.c.isTypeAssignableTo(t, permissiveExtendsType)
4045+
})
4046+
if skipTrue {
4047+
return r.c.getFalseTypeFromConditionalType(target)
4048+
}
4049+
if r.c.isTypeAssignableTo(r.c.getRestrictiveInstantiation(conditional.checkType), r.c.getRestrictiveInstantiation(conditional.extendsType)) {
4050+
return nil
4051+
}
4052+
checkType := getNonDistributedTypeParameter(root.checkType)
4053+
if r.c.isTypeParameterPossiblyReferenced(checkType, root.node.ExtendsType) {
4054+
return nil
4055+
}
4056+
falseType := r.c.getTypeFromTypeNode(root.node.FalseType)
4057+
if !r.canDistributeTypeInstantiation(falseType, checkType) {
4058+
return nil
4059+
}
4060+
return r.c.instantiateType(
4061+
falseType,
4062+
r.c.combineTypeMappers(
4063+
newSimpleTypeMapper(checkType, r.c.getIntersectionType([]*Type{root.checkType, r.c.getNegatedType(root.extendsType)})),
4064+
conditional.mapper,
4065+
),
4066+
)
4067+
}
4068+
4069+
func (r *Relater) canDistributeTypeInstantiation(t *Type, typeParameter *Type) bool {
4070+
key := DistributiveInstantiationKey{typeId: t.id, typeParameterId: typeParameter.id}
4071+
if result, ok := r.c.distributiveInstantiationCache[key]; ok {
4072+
return result
4073+
}
4074+
// A recursive probe is unproven until the outer comparison completes.
4075+
r.c.distributiveInstantiationCache[key] = false
4076+
// Filtering can remove the entire check type, so the false branch must also preserve never.
4077+
if r.c.instantiateType(t, newSimpleTypeMapper(typeParameter, r.c.neverType)).flags&TypeFlagsNever == 0 {
4078+
return false
4079+
}
4080+
left := r.c.markerSuperType
4081+
right := r.c.markerOtherType
4082+
instantiate := func(argument *Type) *Type {
4083+
return r.c.getSimplifiedType(r.c.instantiateType(t, newSimpleTypeMapper(typeParameter, argument)), false /*writing*/)
4084+
}
4085+
// Require execution over unrelated union members to agree with distributing first.
4086+
// Use the existing identity comparison rather than introducing conditional relationship rules.
4087+
instantiatedUnion := instantiate(r.c.getUnionType([]*Type{left, right}))
4088+
unionOfInstantiations := r.c.getUnionType([]*Type{instantiate(left), instantiate(right)})
4089+
result := r.c.isTypeIdenticalTo(instantiatedUnion, unionOfInstantiations)
4090+
r.c.distributiveInstantiationCache[key] = result
4091+
return result
40534092
}
40544093

40554094
func (r *Relater) typeArgumentsRelatedTo(sources []*Type, targets []*Type, variances []VarianceFlags, reportErrors bool, intersectionState IntersectionState) Ternary {

‎tsc/testdata/baselines/reference/compiler/indexMappedTypeWithFilteredKeysWithNegation.js‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,102 @@ const t1: T1 = ['b', 'c'];
3939

4040
type S1<T> = { [K in keyof T & string as K extends '_key' ? never : K]: 1 }[LocalExclude<keyof T & string, '_key'>];
4141
type S2<T> = { [K in string & keyof T as K extends '_key' ? never : K]: 1 }[LocalExclude<string & keyof T, '_key'>];
42+
43+
type ConditionalExclude<T, U> = T extends U ? never : T;
44+
45+
function exclude<T, U>(value: T & not U): ConditionalExclude<T, U> {
46+
return value;
47+
}
48+
49+
type Identity<T> = T;
50+
type AliasedExclude<T, U> = T extends U ? never : Identity<T>;
51+
52+
function aliasedExclude<T, U>(value: T & not U): AliasedExclude<T, U> {
53+
return value;
54+
}
55+
56+
function mappedExclude<T, U>(value: keyof T & not U): keyof ExcludeKey<T, U> {
57+
return value;
58+
}
59+
60+
type AliasedExcludeKey<T, U> = {
61+
[K in keyof T as ConditionalExclude<K, U>]: T[K]
62+
};
63+
64+
function aliasedMappedExclude<T, U>(value: keyof T & not U): keyof AliasedExcludeKey<T, U> {
65+
return value;
66+
}
67+
68+
type Prefixed<T extends string> = T extends "skip" ? never : `prefix${T}`;
69+
70+
function prefixed<T extends string>(value: `prefix${T & not "skip"}`): Prefixed<T> {
71+
return value;
72+
}
73+
74+
type WithSuffix<T extends string, S extends string> = T extends "skip" ? never : `prefix${T}:${S}`;
75+
76+
function withSuffix<T extends string, S extends string>(value: `prefix${T & not "skip"}:${S}`): WithSuffix<T, S> {
77+
return value;
78+
}
79+
80+
type Cased<T extends string> = Uppercase<`prefix${Lowercase<T>}`>;
81+
type FilteredCased<T extends string> = T extends "skip" ? never : Cased<T>;
82+
83+
function cased<T extends string>(value: Cased<T & not "skip">): FilteredCased<T> {
84+
return value;
85+
}
86+
87+
type Alternatives<T extends string> = T extends "skip" ? never : `left${T}` | `right${Uppercase<T>}`;
88+
89+
function alternatives<T extends string>(
90+
value: `left${T & not "skip"}` | `right${Uppercase<T & not "skip">}`
91+
): Alternatives<T> {
92+
return value;
93+
}
94+
95+
type Branded<T> = T extends "skip" ? never : T & { brand: true };
96+
97+
function branded<T>(value: T & not "skip" & { brand: true }): Branded<T> {
98+
return value;
99+
}
100+
101+
interface Values {
102+
a: 1;
103+
b: 2;
104+
skip: 3;
105+
}
106+
107+
type Lookup<T extends keyof Values> = T extends "skip" ? never : Values[T];
108+
109+
function lookup<T extends keyof Values>(value: Values[T & not "skip"]): Lookup<T> {
110+
return value;
111+
}
112+
113+
type PrefixedKeys<T> = {
114+
[K in keyof T & string as K extends "_key" ? never : `prefix${K}`]: T[K]
115+
};
116+
117+
function prefixedKey<T>(value: `prefix${keyof T & string & not "_key"}`): keyof PrefixedKeys<T> {
118+
return value;
119+
}
120+
121+
type FunctionResult<T> = T extends string ? never : () => T;
122+
123+
function falseFunction<T>(value: () => [T]): FunctionResult<[T]> {
124+
return value;
125+
}
126+
127+
type TupleResult<T> = T extends string ? never : [T, T];
128+
129+
function falseTuple<T>(value: [[T], [T]]): TupleResult<[T]> {
130+
return value;
131+
}
132+
133+
type Nested<T extends string> = T extends "skip" ? never : ConditionalExclude<`prefix${T}`, "prefixomit">;
134+
135+
function nested<T extends string>(value: ConditionalExclude<`prefix${T & not "skip"}`, "prefixomit">): Nested<T> {
136+
return value;
137+
}
42138

43139

44140
//// [indexMappedTypeWithFilteredKeysWithNegation.js]
@@ -48,3 +144,45 @@ function f(test, possibleKey) {
48144
test[possibleKey];
49145
}
50146
const t1 = ['b', 'c'];
147+
function exclude(value) {
148+
return value;
149+
}
150+
function aliasedExclude(value) {
151+
return value;
152+
}
153+
function mappedExclude(value) {
154+
return value;
155+
}
156+
function aliasedMappedExclude(value) {
157+
return value;
158+
}
159+
function prefixed(value) {
160+
return value;
161+
}
162+
function withSuffix(value) {
163+
return value;
164+
}
165+
function cased(value) {
166+
return value;
167+
}
168+
function alternatives(value) {
169+
return value;
170+
}
171+
function branded(value) {
172+
return value;
173+
}
174+
function lookup(value) {
175+
return value;
176+
}
177+
function prefixedKey(value) {
178+
return value;
179+
}
180+
function falseFunction(value) {
181+
return value;
182+
}
183+
function falseTuple(value) {
184+
return value;
185+
}
186+
function nested(value) {
187+
return value;
188+
}

0 commit comments

Comments
 (0)