Skip to content

Mark conditional extends as Unmeasurable and use a conditional-opaque wildcard for type erasure #43887

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

Closed
Closed
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
41 changes: 28 additions & 13 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,9 +735,12 @@ namespace ts {
const unknownSymbol = createSymbol(SymbolFlags.Property, "unknown" as __String);
const resolvingSymbol = createSymbol(0, InternalSymbolName.Resolving);

// note: wildcards must be first types constructed due to an assumption made during union construction
// if you reorder these, you must adjust that, too!
const wildcardType = createIntrinsicType(TypeFlags.Any, "any");
const opaqueWildcardType = createIntrinsicType(TypeFlags.Any, "any");
const anyType = createIntrinsicType(TypeFlags.Any, "any");
const autoType = createIntrinsicType(TypeFlags.Any, "any");
const wildcardType = createIntrinsicType(TypeFlags.Any, "any");
const errorType = createIntrinsicType(TypeFlags.Any, "error");
const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType);
const intrinsicMarkerType = createIntrinsicType(TypeFlags.Any, "intrinsic");
Expand Down Expand Up @@ -13629,7 +13632,7 @@ namespace ts {
if (!(flags & TypeFlags.Never)) {
includes |= flags & TypeFlags.IncludesMask;
if (flags & TypeFlags.StructuredOrInstantiable) includes |= TypeFlags.IncludesStructuredOrInstantiable;
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
if (type === wildcardType || type === opaqueWildcardType) includes |= TypeFlags.IncludesWildcard;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this also set the IncludesOpaqueWildcard like the statements abit below?

Copy link
Member Author

Choose a reason for hiding this comment

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

It probably can. I was originally trying to avoid adding an extra includes flag, which, for unions, was possible by exploiting the sort order of the result set; however intersections are unsorted, so I had to add a flag to handle it there.

if (!strictNullChecks && flags & TypeFlags.Nullable) {
if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType;
}
Expand Down Expand Up @@ -13788,7 +13791,10 @@ namespace ts {
const includes = addTypesToUnion(typeSet, 0, types);
if (unionReduction !== UnionReduction.None) {
if (includes & TypeFlags.AnyOrUnknown) {
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
// A wildcard has the _lowest_ type id, as it's first types created, and it's made in priority order
// - a normal `wildcardType` should take priority over an `opaqueWildcardType` if both somehoe end up
Copy link
Collaborator

Choose a reason for hiding this comment

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

somehow* end up

// in the same construct. (And, likewise, both should have priority over a normal `any`)
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? typeSet[0] : anyType : unknownType;
}
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) {
removeRedundantLiteralTypes(typeSet, includes, !!(unionReduction & UnionReduction.Subtype));
Expand Down Expand Up @@ -13927,7 +13933,8 @@ namespace ts {
}
else {
if (flags & TypeFlags.AnyOrUnknown) {
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
if (type === wildcardType || type === opaqueWildcardType) includes |= TypeFlags.IncludesWildcard;
if (type === opaqueWildcardType) includes |= TypeFlags.IncludesOpaqueWildcard;
}
else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !typeSet.has(type.id.toString())) {
if (type.flags & TypeFlags.Unit && includes & TypeFlags.Unit) {
Expand Down Expand Up @@ -14111,7 +14118,7 @@ namespace ts {
return neverType;
}
if (includes & TypeFlags.Any) {
return includes & TypeFlags.IncludesWildcard ? wildcardType : anyType;
return includes & TypeFlags.IncludesWildcard ? includes & TypeFlags.IncludesOpaqueWildcard ? opaqueWildcardType : wildcardType : anyType;
}
if (!strictNullChecks && includes & TypeFlags.Nullable) {
return includes & TypeFlags.Undefined ? undefinedType : nullType;
Expand Down Expand Up @@ -14313,6 +14320,7 @@ namespace ts {
type.flags & TypeFlags.InstantiableNonPrimitive || isGenericTupleType(type) || isGenericMappedType(type) && maybeNonDistributiveNameType(getNameTypeFromMappedType(type)) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(<MappedType>type, noIndexSignatures) :
type === wildcardType ? wildcardType :
type === opaqueWildcardType ? opaqueWildcardType :
type.flags & TypeFlags.Unknown ? neverType :
type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType :
stringsOnly ? !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? stringType : getLiteralTypeFromProperties(type, TypeFlags.StringLiteral, includeOrigin) :
Expand Down Expand Up @@ -14373,6 +14381,9 @@ namespace ts {
mapType(types[unionIndex], t => getTemplateLiteralType(texts, replaceElement(types, unionIndex, t))) :
errorType;
}
if (contains(types, opaqueWildcardType)) {
return opaqueWildcardType;
}
if (contains(types, wildcardType)) {
return wildcardType;
}
Expand Down Expand Up @@ -14890,6 +14901,9 @@ namespace ts {
}

function getIndexedAccessTypeOrUndefined(objectType: Type, indexType: Type, noUncheckedIndexedAccessCandidate?: boolean, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression, accessFlags = AccessFlags.None, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type | undefined {
if (objectType === opaqueWildcardType || indexType === opaqueWildcardType) {
return wildcardType;
}
if (objectType === wildcardType || indexType === wildcardType) {
return wildcardType;
}
Expand Down Expand Up @@ -15023,7 +15037,7 @@ namespace ts {
while (true) {
const isUnwrapped = isTypicalNondistributiveConditional(root);
const checkType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.checkType), mapper);
const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType);
const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType) || checkType === opaqueWildcardType;
const extendsType = instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), mapper);
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
Expand All @@ -15045,7 +15059,7 @@ namespace ts {
// Instantiate the extends type including inferences for 'infer T' type parameters
const inferredExtendsType = combinedMapper ? instantiateType(unwrapNondistributiveConditionalTuple(root, root.extendsType), combinedMapper) : extendsType;
// We attempt to resolve the conditional type only when the check and extends types are non-generic
if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) {
if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType) && inferredExtendsType !== opaqueWildcardType) {
// Return falseType for a definitely false extends check. We check an instantiations of the two
// types with type parameters mapped to the wildcard type, the most permissive instantiations
// possible (the wildcard type is assignable to and from all types). If those are not related,
Expand Down Expand Up @@ -15782,7 +15796,7 @@ namespace ts {
}

function createTypeEraser(sources: readonly TypeParameter[]): TypeMapper {
return createTypeMapper(sources, /*targets*/ undefined);
return createTypeMapper(sources, map(sources, () => opaqueWildcardType));
}

/**
Expand Down Expand Up @@ -15997,7 +16011,7 @@ namespace ts {
const mappedTypeVariable = instantiateType(typeVariable, mapper);
if (typeVariable !== mappedTypeVariable) {
return mapTypeWithAlias(getReducedType(mappedTypeVariable), t => {
if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && t !== errorType) {
if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && t !== opaqueWildcardType && t !== errorType) {
if (!type.declaration.nameType) {
if (isArrayType(t)) {
return instantiateMappedArrayType(t, type, prependTypeMapping(typeVariable, t, mapper));
Expand All @@ -16016,7 +16030,8 @@ namespace ts {
}
}
// If the constraint type of the instantiation is the wildcard type, return the wildcard type.
return instantiateType(getConstraintTypeFromMappedType(type), mapper) === wildcardType ? wildcardType : instantiateAnonymousType(type, mapper, aliasSymbol, aliasTypeArguments);
const instantiatedConstraint = instantiateType(getConstraintTypeFromMappedType(type), mapper);
return instantiatedConstraint === opaqueWildcardType ? opaqueWildcardType : instantiatedConstraint === wildcardType ? wildcardType : instantiateAnonymousType(type, mapper, aliasSymbol, aliasTypeArguments);
}

function getModifiedReadonlyState(state: boolean, modifiers: MappedTypeModifiers) {
Expand Down Expand Up @@ -17161,7 +17176,7 @@ namespace ts {
function isSimpleTypeRelatedTo(source: Type, target: Type, relation: ESMap<string, RelationComparisonResult>, errorReporter?: ErrorReporter) {
const s = source.flags;
const t = target.flags;
if (t & TypeFlags.AnyOrUnknown || s & TypeFlags.Never || source === wildcardType) return true;
if (t & TypeFlags.AnyOrUnknown || s & TypeFlags.Never || source === wildcardType || source === opaqueWildcardType) return true;
if (t & TypeFlags.Never) return false;
if (s & TypeFlags.StringLike && t & TypeFlags.String) return true;
if (s & TypeFlags.StringLiteral && s & TypeFlags.EnumLiteral &&
Expand Down Expand Up @@ -18546,7 +18561,7 @@ namespace ts {
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
const sourceParams = (source as ConditionalType).root.inferTypeParameters;
let sourceExtends = (<ConditionalType>source).extendsType;
let sourceExtends = instantiateType((<ConditionalType>source).extendsType, makeFunctionTypeMapper(reportUnmeasurableMarkers));
let mapper: TypeMapper | undefined;
if (sourceParams) {
// If the source has infer type parameters, we instantiate them in the context of the target
Expand Down Expand Up @@ -21035,7 +21050,7 @@ namespace ts {
if (!couldContainTypeVariables(target)) {
return;
}
if (source === wildcardType) {
if (source === wildcardType || source === opaqueWildcardType) {
// We are inferring from an 'any' type. We want to infer this type for every type parameter
// referenced in the target type, so we record it as the propagation type and infer from the
// target to itself. Then, as we find candidates we substitute the propagation type.
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5074,6 +5074,8 @@ namespace ts {
IncludesWildcard = IndexedAccess,
/* @internal */
IncludesEmptyObject = Conditional,
/* @internal */
IncludesOpaqueWildcard = StringMapping,
}

export type DestructuringPattern = BindingPattern | ObjectLiteralExpression | ArrayLiteralExpression;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ declare module Immutable {
>Seq : typeof Seq

function isSeq(maybeSeq: any): maybeSeq is Seq.Indexed<any> | Seq.Keyed<any, any>;
>isSeq : (maybeSeq: any) => maybeSeq is Indexed<any> | Keyed<any, any>
>isSeq : (maybeSeq: any) => maybeSeq is Keyed<any, any> | Indexed<any>
>maybeSeq : any
>Seq : any
>Seq : any
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
tests/cases/compiler/emptyClassSomehowNeverChecksConditionals.ts(5,7): error TS2322: Type 'EqualsTest<string>' is not assignable to type 'EqualsTest<number>'.
Type 'any extends string ? 1 : 0' is not assignable to type 'any extends number ? 1 : 0'.
Type '0 | 1' is not assignable to type 'any extends number ? 1 : 0'.
Type '0' is not assignable to type 'any extends number ? 1 : 0'.
tests/cases/compiler/emptyClassSomehowNeverChecksConditionals.ts(6,7): error TS2322: Type 'EqualsTest1<string>' is not assignable to type 'EqualsTest<number>'.
Type 'A extends string ? 1 : 0' is not assignable to type 'A extends number ? 1 : 0'.
Type '0 | 1' is not assignable to type 'A extends number ? 1 : 0'.
Type '0' is not assignable to type 'A extends number ? 1 : 0'.
tests/cases/compiler/emptyClassSomehowNeverChecksConditionals.ts(12,27): error TS2344: Type 'this' does not satisfy the constraint 'Model<typeof Model>'.
Type 'Model<MClass>' is not assignable to type 'Model<typeof Model>'.
Types of property 'set' are incompatible.
Type '<K>(value: K extends MClass ? number : string) => void' is not assignable to type '<K>(value: K extends typeof Model ? number : string) => void'.
Types of parameters 'value' and 'value' are incompatible.
Type 'K extends typeof Model ? number : string' is not assignable to type 'K extends MClass ? number : string'.
Type 'string | number' is not assignable to type 'K extends MClass ? number : string'.
Type 'string' is not assignable to type 'K extends MClass ? number : string'.
tests/cases/compiler/emptyClassSomehowNeverChecksConditionals.ts(20,28): error TS2344: Type 'this' does not satisfy the constraint 'ModelSub'.
Type 'Model2<MClass>' is not assignable to type 'Model2<typeof ModelSub>'.
Types of property 'set' are incompatible.
Type '<K>(value: K extends MClass ? number : string) => void' is not assignable to type '<K>(value: K extends typeof ModelSub ? number : string) => void'.
Types of parameters 'value' and 'value' are incompatible.
Type 'K extends typeof ModelSub ? number : string' is not assignable to type 'K extends MClass ? number : string'.
Type 'string | number' is not assignable to type 'K extends MClass ? number : string'.
Type 'string' is not assignable to type 'K extends MClass ? number : string'.


==== tests/cases/compiler/emptyClassSomehowNeverChecksConditionals.ts (4 errors) ====
// quick distillation of conditionals which were previously erased by signature relating
type EqualsTest<T> = <A>() => A extends T ? 1 : 0;
type EqualsTest1<T> = <A>() => A extends T ? 1 : 0;

const x: EqualsTest<number> = undefined as any as EqualsTest<string>; // should error, obviously wrong
~
!!! error TS2322: Type 'EqualsTest<string>' is not assignable to type 'EqualsTest<number>'.
!!! error TS2322: Type 'any extends string ? 1 : 0' is not assignable to type 'any extends number ? 1 : 0'.
!!! error TS2322: Type '0 | 1' is not assignable to type 'any extends number ? 1 : 0'.
!!! error TS2322: Type '0' is not assignable to type 'any extends number ? 1 : 0'.
const y: EqualsTest<number> = undefined as any as EqualsTest1<string>; // same as the above, but seperate type aliases
~
!!! error TS2322: Type 'EqualsTest1<string>' is not assignable to type 'EqualsTest<number>'.
!!! error TS2322: Type 'A extends string ? 1 : 0' is not assignable to type 'A extends number ? 1 : 0'.
!!! error TS2322: Type '0 | 1' is not assignable to type 'A extends number ? 1 : 0'.
!!! error TS2322: Type '0' is not assignable to type 'A extends number ? 1 : 0'.

// Slightly extended example using class inheritance
type ModelId<M extends Model> = M; // just validates the input matches the `Model` type to issue an error
export declare class Model<MClass extends typeof Model = typeof Model> {
class: MClass;
readonly ref: ModelId<this>;
~~~~
!!! error TS2344: Type 'this' does not satisfy the constraint 'Model<typeof Model>'.
!!! error TS2344: Type 'Model<MClass>' is not assignable to type 'Model<typeof Model>'.
!!! error TS2344: Types of property 'set' are incompatible.
!!! error TS2344: Type '<K>(value: K extends MClass ? number : string) => void' is not assignable to type '<K>(value: K extends typeof Model ? number : string) => void'.
!!! error TS2344: Types of parameters 'value' and 'value' are incompatible.
!!! error TS2344: Type 'K extends typeof Model ? number : string' is not assignable to type 'K extends MClass ? number : string'.
!!! error TS2344: Type 'string | number' is not assignable to type 'K extends MClass ? number : string'.
!!! error TS2344: Type 'string' is not assignable to type 'K extends MClass ? number : string'.
set<K>(value: K extends MClass ? number : string): void;
}

// identical to the above, but with a no-op subclass
type ModelId2<M extends ModelSub> = M;
export declare class Model2<MClass extends typeof ModelSub = typeof ModelSub> {
class: MClass;
readonly ref: ModelId2<this>;
~~~~
!!! error TS2344: Type 'this' does not satisfy the constraint 'ModelSub'.
!!! error TS2344: Type 'Model2<MClass>' is not assignable to type 'Model2<typeof ModelSub>'.
!!! error TS2344: Types of property 'set' are incompatible.
!!! error TS2344: Type '<K>(value: K extends MClass ? number : string) => void' is not assignable to type '<K>(value: K extends typeof ModelSub ? number : string) => void'.
!!! error TS2344: Types of parameters 'value' and 'value' are incompatible.
!!! error TS2344: Type 'K extends typeof ModelSub ? number : string' is not assignable to type 'K extends MClass ? number : string'.
!!! error TS2344: Type 'string | number' is not assignable to type 'K extends MClass ? number : string'.
!!! error TS2344: Type 'string' is not assignable to type 'K extends MClass ? number : string'.
set<K>(value: K extends MClass ? number : string): void;
}
export declare class ModelSub extends Model2 {}
Loading