Skip to content

Fix partial type relations #17382

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 4 commits into from
Jul 28, 2017
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
59 changes: 25 additions & 34 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5765,12 +5765,13 @@ namespace ts {
return type.modifiersType;
}

function isPartialMappedType(type: Type) {
return getObjectFlags(type) & ObjectFlags.Mapped && !!(<MappedType>type).declaration.questionToken;
}
Copy link
Member

@DanielRosenwasser DanielRosenwasser Jul 26, 2017

Choose a reason for hiding this comment

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

This seems like an awfully specific check - does it work on homomorphic mapped types in which all properties of the type being mapped are already optional? For example:

type Shapeify<T> = {
    [K in keyof T]: any;
}

interface MyInterface {
  something?: number;
}

class MyClass<T extends MyInterface> {
  doIt(data: Shapeify<T>) {}
}

class MySubClass extends MyClass<MyInterface> {}

function fn(arg: typeof MyClass) {};

fn(MySubClass);

The same question applies to if you used Readonly<T> or others.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what you're getting at. The only way we can be certain that all properties of the type that results from a mapping operation are optional is if the mapped type includes a ? in the template specification. That's what we're checking here.

Copy link
Member

Choose a reason for hiding this comment

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

As we discussed offline, that example doesn't apply because a constraint being all-optional (e.g. MyInterface) doesn't guarantee that T itself will be all-optional.


function isGenericMappedType(type: Type) {
if (getObjectFlags(type) & ObjectFlags.Mapped) {
const constraintType = getConstraintTypeFromMappedType(<MappedType>type);
return maybeTypeOfKind(constraintType, TypeFlags.TypeVariable | TypeFlags.Index);
}
return false;
return getObjectFlags(type) & ObjectFlags.Mapped &&
maybeTypeOfKind(getConstraintTypeFromMappedType(<MappedType>type), TypeFlags.TypeVariable | TypeFlags.Index);
}

function resolveStructuredTypeMembers(type: StructuredType): ResolvedType {
Expand Down Expand Up @@ -9254,8 +9255,12 @@ namespace ts {
if (source.flags & (TypeFlags.Object | TypeFlags.Intersection) && target.flags & TypeFlags.Object) {
// Report structural errors only if we haven't reported any errors yet
const reportStructuralErrors = reportErrors && errorInfo === saveErrorInfo && !sourceIsPrimitive;
if (isGenericMappedType(source) || isGenericMappedType(target)) {
result = mappedTypeRelatedTo(source, target, reportStructuralErrors);
// An empty object type is related to any mapped type that includes a '?' modifier.
if (isPartialMappedType(target) && !isGenericMappedType(source) && isEmptyObjectType(source)) {
result = Ternary.True;
}
else if (isGenericMappedType(target)) {
result = isGenericMappedType(source) ? mappedTypeRelatedTo(<MappedType>source, <MappedType>target, reportStructuralErrors) : Ternary.False;
Copy link
Member

Choose a reason for hiding this comment

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

If you made isGenericMappedType a type predicate, you could avoid the type assertions.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, because something for which isGenericMappedType returns false could still be a MappedType. We would need to introduce a new unique type representing generic mapped types, but it isn't worth the hassle.

}
else {
result = propertiesRelatedTo(source, target, reportStructuralErrors);
Expand Down Expand Up @@ -9284,33 +9289,19 @@ namespace ts {
// A type [P in S]: X is related to a type [Q in T]: Y if T is related to S and X' is
// related to Y, where X' is an instantiation of X in which P is replaced with Q. Notice
// that S and T are contra-variant whereas X and Y are co-variant.
function mappedTypeRelatedTo(source: Type, target: Type, reportErrors: boolean): Ternary {
if (isGenericMappedType(target)) {
if (isGenericMappedType(source)) {
const sourceReadonly = !!(<MappedType>source).declaration.readonlyToken;
const sourceOptional = !!(<MappedType>source).declaration.questionToken;
const targetReadonly = !!(<MappedType>target).declaration.readonlyToken;
const targetOptional = !!(<MappedType>target).declaration.questionToken;
const modifiersRelated = relation === identityRelation ?
sourceReadonly === targetReadonly && sourceOptional === targetOptional :
relation === comparableRelation || !sourceOptional || targetOptional;
if (modifiersRelated) {
let result: Ternary;
if (result = isRelatedTo(getConstraintTypeFromMappedType(<MappedType>target), getConstraintTypeFromMappedType(<MappedType>source), reportErrors)) {
const mapper = createTypeMapper([getTypeParameterFromMappedType(<MappedType>source)], [getTypeParameterFromMappedType(<MappedType>target)]);
return result & isRelatedTo(instantiateType(getTemplateTypeFromMappedType(<MappedType>source), mapper), getTemplateTypeFromMappedType(<MappedType>target), reportErrors);
}
}
}
else if ((<MappedType>target).declaration.questionToken && isEmptyObjectType(source)) {
return Ternary.True;

}
}
else if (relation !== identityRelation) {
const resolved = resolveStructuredTypeMembers(<ObjectType>target);
if (isEmptyResolvedType(resolved) || resolved.stringIndexInfo && resolved.stringIndexInfo.type.flags & TypeFlags.Any) {
return Ternary.True;
function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary {
const sourceReadonly = !!source.declaration.readonlyToken;
const sourceOptional = !!source.declaration.questionToken;
const targetReadonly = !!target.declaration.readonlyToken;
const targetOptional = !!target.declaration.questionToken;
const modifiersRelated = relation === identityRelation ?
sourceReadonly === targetReadonly && sourceOptional === targetOptional :
relation === comparableRelation || !sourceOptional || targetOptional;
if (modifiersRelated) {
let result: Ternary;
if (result = isRelatedTo(getConstraintTypeFromMappedType(<MappedType>target), getConstraintTypeFromMappedType(<MappedType>source), reportErrors)) {
const mapper = createTypeMapper([getTypeParameterFromMappedType(<MappedType>source)], [getTypeParameterFromMappedType(<MappedType>target)]);
return result & isRelatedTo(instantiateType(getTemplateTypeFromMappedType(<MappedType>source), mapper), getTemplateTypeFromMappedType(<MappedType>target), reportErrors);
}
}
return Ternary.False;
Expand Down
46 changes: 46 additions & 0 deletions tests/baselines/reference/mappedTypePartialConstraints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//// [mappedTypePartialConstraints.ts]
// Repro from #16985

interface MyInterface {
something: number;
}

class MyClass<T extends MyInterface> {
doIt(data : Partial<T>) {}
}

class MySubClass extends MyClass<MyInterface> {}

function fn(arg: typeof MyClass) {};

fn(MySubClass);


//// [mappedTypePartialConstraints.js]
// Repro from #16985
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var MyClass = (function () {
function MyClass() {
}
MyClass.prototype.doIt = function (data) { };
return MyClass;
}());
var MySubClass = (function (_super) {
__extends(MySubClass, _super);
function MySubClass() {
return _super !== null && _super.apply(this, arguments) || this;
}
return MySubClass;
}(MyClass));
function fn(arg) { }
;
fn(MySubClass);
36 changes: 36 additions & 0 deletions tests/baselines/reference/mappedTypePartialConstraints.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
=== tests/cases/compiler/mappedTypePartialConstraints.ts ===
// Repro from #16985

interface MyInterface {
>MyInterface : Symbol(MyInterface, Decl(mappedTypePartialConstraints.ts, 0, 0))

something: number;
>something : Symbol(MyInterface.something, Decl(mappedTypePartialConstraints.ts, 2, 23))
}

class MyClass<T extends MyInterface> {
>MyClass : Symbol(MyClass, Decl(mappedTypePartialConstraints.ts, 4, 1))
>T : Symbol(T, Decl(mappedTypePartialConstraints.ts, 6, 14))
>MyInterface : Symbol(MyInterface, Decl(mappedTypePartialConstraints.ts, 0, 0))

doIt(data : Partial<T>) {}
>doIt : Symbol(MyClass.doIt, Decl(mappedTypePartialConstraints.ts, 6, 38))
>data : Symbol(data, Decl(mappedTypePartialConstraints.ts, 7, 7))
>Partial : Symbol(Partial, Decl(lib.d.ts, --, --))
>T : Symbol(T, Decl(mappedTypePartialConstraints.ts, 6, 14))
}

class MySubClass extends MyClass<MyInterface> {}
>MySubClass : Symbol(MySubClass, Decl(mappedTypePartialConstraints.ts, 8, 1))
>MyClass : Symbol(MyClass, Decl(mappedTypePartialConstraints.ts, 4, 1))
>MyInterface : Symbol(MyInterface, Decl(mappedTypePartialConstraints.ts, 0, 0))

function fn(arg: typeof MyClass) {};
>fn : Symbol(fn, Decl(mappedTypePartialConstraints.ts, 10, 48))
>arg : Symbol(arg, Decl(mappedTypePartialConstraints.ts, 12, 12))
>MyClass : Symbol(MyClass, Decl(mappedTypePartialConstraints.ts, 4, 1))

fn(MySubClass);
>fn : Symbol(fn, Decl(mappedTypePartialConstraints.ts, 10, 48))
>MySubClass : Symbol(MySubClass, Decl(mappedTypePartialConstraints.ts, 8, 1))

37 changes: 37 additions & 0 deletions tests/baselines/reference/mappedTypePartialConstraints.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
=== tests/cases/compiler/mappedTypePartialConstraints.ts ===
// Repro from #16985

interface MyInterface {
>MyInterface : MyInterface

something: number;
>something : number
}

class MyClass<T extends MyInterface> {
>MyClass : MyClass<T>
>T : T
>MyInterface : MyInterface

doIt(data : Partial<T>) {}
>doIt : (data: Partial<T>) => void
>data : Partial<T>
>Partial : Partial<T>
>T : T
}

class MySubClass extends MyClass<MyInterface> {}
>MySubClass : MySubClass
>MyClass : MyClass<MyInterface>
>MyInterface : MyInterface

function fn(arg: typeof MyClass) {};
>fn : (arg: typeof MyClass) => void
>arg : typeof MyClass
>MyClass : typeof MyClass

fn(MySubClass);
>fn(MySubClass) : void
>fn : (arg: typeof MyClass) => void
>MySubClass : typeof MySubClass

37 changes: 21 additions & 16 deletions tests/baselines/reference/mappedTypeRelationships.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,25 +60,26 @@ tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(51,5): error TS2
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(56,5): error TS2542: Index signature in type 'Readonly<T>' only permits reading.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(61,5): error TS2542: Index signature in type 'Readonly<U>' only permits reading.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(66,5): error TS2542: Index signature in type 'Readonly<U>' only permits reading.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(70,5): error TS2322: Type 'Partial<T>' is not assignable to type 'T'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(75,5): error TS2322: Type 'Partial<T>' is not assignable to type 'T'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(125,5): error TS2322: Type 'Partial<U>' is not assignable to type 'Identity<U>'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(141,5): error TS2322: Type '{ [P in keyof T]: T[P]; }' is not assignable to type '{ [P in keyof T]: U[P]; }'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(72,5): error TS2322: Type 'Partial<T>' is not assignable to type 'T'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(78,5): error TS2322: Type 'Partial<Thing>' is not assignable to type 'Partial<T>'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(88,5): error TS2322: Type 'Readonly<Thing>' is not assignable to type 'Readonly<T>'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(127,5): error TS2322: Type 'Partial<U>' is not assignable to type 'Identity<U>'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(143,5): error TS2322: Type '{ [P in keyof T]: T[P]; }' is not assignable to type '{ [P in keyof T]: U[P]; }'.
Type 'T[P]' is not assignable to type 'U[P]'.
Type 'T[string]' is not assignable to type 'U[P]'.
Type 'T[string]' is not assignable to type 'U[string]'.
Type 'T[P]' is not assignable to type 'U[string]'.
Type 'T[string]' is not assignable to type 'U[string]'.
Type 'T' is not assignable to type 'U'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(146,5): error TS2322: Type '{ [P in keyof T]: T[P]; }' is not assignable to type '{ [P in keyof U]: U[P]; }'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(148,5): error TS2322: Type '{ [P in keyof T]: T[P]; }' is not assignable to type '{ [P in keyof U]: U[P]; }'.
Type 'keyof U' is not assignable to type 'keyof T'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(151,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in keyof T]: T[P]; }'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(153,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in keyof T]: T[P]; }'.
Type 'keyof T' is not assignable to type 'K'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(156,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in keyof U]: U[P]; }'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(158,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in keyof U]: U[P]; }'.
Type 'keyof U' is not assignable to type 'K'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(161,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in keyof T]: U[P]; }'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(163,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in keyof T]: U[P]; }'.
Type 'keyof T' is not assignable to type 'K'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(166,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in K]: U[P]; }'.
tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(168,5): error TS2322: Type '{ [P in K]: T[P]; }' is not assignable to type '{ [P in K]: U[P]; }'.
Type 'T[P]' is not assignable to type 'U[P]'.
Type 'T[string]' is not assignable to type 'U[P]'.
Type 'T[string]' is not assignable to type 'U[string]'.
Expand All @@ -87,7 +88,7 @@ tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(166,5): error TS
Type 'T' is not assignable to type 'U'.


==== tests/cases/conformance/types/mapped/mappedTypeRelationships.ts (27 errors) ====
==== tests/cases/conformance/types/mapped/mappedTypeRelationships.ts (28 errors) ====
function f1<T>(x: T, k: keyof T) {
return x[k];
}
Expand Down Expand Up @@ -236,28 +237,32 @@ tests/cases/conformance/types/mapped/mappedTypeRelationships.ts(166,5): error TS
!!! error TS2542: Index signature in type 'Readonly<U>' only permits reading.
}

type Thing = { a: string, b: string };

function f30<T>(x: T, y: Partial<T>) {
x = y; // Error
~
!!! error TS2322: Type 'Partial<T>' is not assignable to type 'T'.
y = x;
}

function f31<T>(x: T, y: Partial<T>) {
x = y; // Error
function f31<T extends Thing>(x: Partial<Thing>, y: Partial<T>) {
x = y;
y = x; // Error
~
!!! error TS2322: Type 'Partial<T>' is not assignable to type 'T'.
y = x;
!!! error TS2322: Type 'Partial<Thing>' is not assignable to type 'Partial<T>'.
}

function f40<T>(x: T, y: Readonly<T>) {
x = y;
y = x;
}

function f41<T>(x: T, y: Readonly<T>) {
function f41<T extends Thing>(x: Readonly<Thing>, y: Readonly<T>) {
x = y;
y = x;
y = x; // Error
~
!!! error TS2322: Type 'Readonly<Thing>' is not assignable to type 'Readonly<T>'.
}

type Item = {
Expand Down
26 changes: 16 additions & 10 deletions tests/baselines/reference/mappedTypeRelationships.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,26 @@ function f23<T, U extends T, K extends keyof T>(x: T, y: Readonly<U>, k: K) {
y[k] = x[k]; // Error
}

type Thing = { a: string, b: string };

function f30<T>(x: T, y: Partial<T>) {
x = y; // Error
y = x;
}

function f31<T>(x: T, y: Partial<T>) {
x = y; // Error
y = x;
function f31<T extends Thing>(x: Partial<Thing>, y: Partial<T>) {
x = y;
y = x; // Error
}

function f40<T>(x: T, y: Readonly<T>) {
x = y;
y = x;
}

function f41<T>(x: T, y: Readonly<T>) {
function f41<T extends Thing>(x: Readonly<Thing>, y: Readonly<T>) {
x = y;
y = x;
y = x; // Error
}

type Item = {
Expand Down Expand Up @@ -228,16 +230,16 @@ function f30(x, y) {
y = x;
}
function f31(x, y) {
x = y; // Error
y = x;
x = y;
y = x; // Error
}
function f40(x, y) {
x = y;
y = x;
}
function f41(x, y) {
x = y;
y = x;
y = x; // Error
}
function f50(obj, key) {
var item = obj[key];
Expand Down Expand Up @@ -304,10 +306,14 @@ declare function f20<T>(x: T, y: Readonly<T>, k: keyof T): void;
declare function f21<T, K extends keyof T>(x: T, y: Readonly<T>, k: K): void;
declare function f22<T, U extends T>(x: T, y: Readonly<U>, k: keyof T): void;
declare function f23<T, U extends T, K extends keyof T>(x: T, y: Readonly<U>, k: K): void;
declare type Thing = {
a: string;
b: string;
};
declare function f30<T>(x: T, y: Partial<T>): void;
declare function f31<T>(x: T, y: Partial<T>): void;
declare function f31<T extends Thing>(x: Partial<Thing>, y: Partial<T>): void;
declare function f40<T>(x: T, y: Readonly<T>): void;
declare function f41<T>(x: T, y: Readonly<T>): void;
declare function f41<T extends Thing>(x: Readonly<Thing>, y: Readonly<T>): void;
declare type Item = {
name: string;
};
Expand Down
15 changes: 15 additions & 0 deletions tests/cases/compiler/mappedTypePartialConstraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Repro from #16985

interface MyInterface {
something: number;
}

class MyClass<T extends MyInterface> {
doIt(data : Partial<T>) {}
}

class MySubClass extends MyClass<MyInterface> {}

function fn(arg: typeof MyClass) {};

fn(MySubClass);
Loading