Skip to content

Commit

Permalink
Merge pull request #31221 from microsoft/improveReverseMappedTypes
Browse files Browse the repository at this point in the history
Improve reverse mapped types
  • Loading branch information
ahejlsberg committed May 10, 2019
2 parents bca2808 + c104aa1 commit ae3d1d4
Show file tree
Hide file tree
Showing 10 changed files with 873 additions and 26 deletions.
21 changes: 17 additions & 4 deletions src/compiler/checker.ts
Expand Up @@ -14921,10 +14921,19 @@ namespace ts {
return type;
}

// We consider a type to be partially inferable if it isn't marked non-inferable or if it is
// an object literal type with at least one property of an inferable type. For example, an object
// literal { a: 123, b: x => true } is marked non-inferable because it contains a context sensitive
// arrow function, but is considered partially inferable because property 'a' has an inferable type.
function isPartiallyInferableType(type: Type): boolean {
return !(getObjectFlags(type) & ObjectFlags.NonInferrableType) ||
isObjectLiteralType(type) && some(getPropertiesOfType(type), prop => isPartiallyInferableType(getTypeOfSymbol(prop)));
}

function createReverseMappedType(source: Type, target: MappedType, constraint: IndexType) {
// If any property contains context sensitive functions that have been skipped, the source type
// is incomplete and we can't infer a meaningful input type.
if (getObjectFlags(source) & ObjectFlags.NonInferrableType || getPropertiesOfType(source).length === 0 && !getIndexInfoOfType(source, IndexKind.String)) {
// We consider a source type reverse mappable if it has a string index signature or if
// it has one or more properties and is of a partially inferable type.
if (!(getIndexInfoOfType(source, IndexKind.String) || getPropertiesOfType(source).length !== 0 && isPartiallyInferableType(source))) {
return undefined;
}
// For arrays and tuples we infer new arrays and tuples where the reverse mapping has been
Expand Down Expand Up @@ -15309,7 +15318,11 @@ namespace ts {
const inferredType = inferTypeForHomomorphicMappedType(source, target, <IndexType>constraintType);
if (inferredType) {
const savePriority = priority;
priority |= InferencePriority.HomomorphicMappedType;
// We assign a lower priority to inferences made from types containing non-inferrable
// types because we may only have a partial result (i.e. we may have failed to make
// reverse inferences for some properties).
priority |= getObjectFlags(source) & ObjectFlags.NonInferrableType ?
InferencePriority.PartialHomomorphicMappedType : InferencePriority.HomomorphicMappedType;
inferFromTypes(inferredType, inference.typeParameter);
priority = savePriority;
}
Expand Down
19 changes: 10 additions & 9 deletions src/compiler/types.ts
Expand Up @@ -4424,15 +4424,16 @@ namespace ts {
export type TypeMapper = (t: TypeParameter) => Type;

export const enum InferencePriority {
NakedTypeVariable = 1 << 0, // Naked type variable in union or intersection type
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
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

PriorityImpliesCombination = ReturnType | MappedTypeConstraint | LiteralKeyof, // These priorities imply that the resulting type should be a combination of all candidates
NakedTypeVariable = 1 << 0, // Naked type variable in union or intersection type
HomomorphicMappedType = 1 << 1, // Reverse inference for homomorphic mapped type
PartialHomomorphicMappedType = 1 << 2, // Partial reverse inference for homomorphic mapped type
MappedTypeConstraint = 1 << 3, // Reverse inference for mapped type
ReturnType = 1 << 4, // Inference made from return type of generic function
LiteralKeyof = 1 << 5, // Inference made from a string literal to a keyof T
NoConstraints = 1 << 6, // Don't infer from constraints of instantiable types
AlwaysStrict = 1 << 7, // Always use strict rules for contravariant inferences

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

/* @internal */
Expand Down
13 changes: 7 additions & 6 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Expand Up @@ -2418,12 +2418,13 @@ declare namespace ts {
enum InferencePriority {
NakedTypeVariable = 1,
HomomorphicMappedType = 2,
MappedTypeConstraint = 4,
ReturnType = 8,
LiteralKeyof = 16,
NoConstraints = 32,
AlwaysStrict = 64,
PriorityImpliesCombination = 28
PartialHomomorphicMappedType = 4,
MappedTypeConstraint = 8,
ReturnType = 16,
LiteralKeyof = 32,
NoConstraints = 64,
AlwaysStrict = 128,
PriorityImpliesCombination = 56
}
/** @deprecated Use FileExtensionInfo instead. */
type JsFileExtensionInfo = FileExtensionInfo;
Expand Down
13 changes: 7 additions & 6 deletions tests/baselines/reference/api/typescript.d.ts
Expand Up @@ -2418,12 +2418,13 @@ declare namespace ts {
enum InferencePriority {
NakedTypeVariable = 1,
HomomorphicMappedType = 2,
MappedTypeConstraint = 4,
ReturnType = 8,
LiteralKeyof = 16,
NoConstraints = 32,
AlwaysStrict = 64,
PriorityImpliesCombination = 28
PartialHomomorphicMappedType = 4,
MappedTypeConstraint = 8,
ReturnType = 16,
LiteralKeyof = 32,
NoConstraints = 64,
AlwaysStrict = 128,
PriorityImpliesCombination = 56
}
/** @deprecated Use FileExtensionInfo instead. */
type JsFileExtensionInfo = FileExtensionInfo;
Expand Down
Expand Up @@ -20,7 +20,7 @@ tests/cases/conformance/types/mapped/mappedTypeInferenceErrors.ts(16,9): error T
baz: 42
~~~
!!! error TS2322: Type 'number' is not assignable to type '() => unknown'.
!!! related TS6500 tests/cases/conformance/types/mapped/mappedTypeInferenceErrors.ts:16:9: The expected type comes from property 'baz' which is declared here on type 'ComputedOf<{ bar: number; baz: unknown; }>'
!!! related TS6500 tests/cases/conformance/types/mapped/mappedTypeInferenceErrors.ts:16:9: The expected type comes from property 'baz' which is declared here on type 'ComputedOf<{ bar: unknown; baz: unknown; }>'
}
});

@@ -0,0 +1,101 @@
tests/cases/compiler/reverseMappedPartiallyInferableTypes.ts(91,20): error TS2571: Object is of type 'unknown'.


==== tests/cases/compiler/reverseMappedPartiallyInferableTypes.ts (1 errors) ====
// Repro from #30505

export type Prop<T> = { (): T }
export type PropType<T> = Prop<T>;
export type PropDefaultValue<T> = T;


export type PropValidatorFunction<T> = (value: T) => boolean;
export type PropValidator<T> = PropOptions<T>;


export type PropOptions<T> = {
type: PropType<T>;

value?: PropDefaultValue<T>,
required?: boolean;
validator?: PropValidatorFunction<T>;
}

export type RecordPropsDefinition<T> = {
[K in keyof T]: PropValidator<T[K]>
}
export type PropsDefinition<T> = RecordPropsDefinition<T>;


declare function extend<T>({ props }: { props: PropsDefinition<T> }): PropsDefinition<T>;

interface MyType {
valid: boolean;
}

const r = extend({
props: {
notResolved: {
type: Object as PropType<MyType>,
validator: x => {
return x.valid;
}
},
explicit: {
type: Object as PropType<MyType>,
validator: (x: MyType) => {
return x.valid;
}
}
}
})

r.explicit
r.notResolved
r.explicit.required
r.notResolved.required

// Modified repro from #30505

type Box<T> = {
contents?: T;
contains?(content: T): boolean;
};

type Mapped<T> = {
[K in keyof T]: Box<T[K]>;
}

declare function id<T>(arg: Mapped<T>): Mapped<T>;

// All properties have inferable types

const obj1 = id({
foo: {
contents: ""
}
});

// Some properties have inferable types

const obj2 = id({
foo: {
contents: "",
contains(k) {
return k.length > 0;
}
}
});

// No properties have inferable types

const obj3 = id({
foo: {
contains(k) {
return k.length > 0;
~
!!! error TS2571: Object is of type 'unknown'.
}
}
});

144 changes: 144 additions & 0 deletions tests/baselines/reference/reverseMappedPartiallyInferableTypes.js
@@ -0,0 +1,144 @@
//// [reverseMappedPartiallyInferableTypes.ts]
// Repro from #30505

export type Prop<T> = { (): T }
export type PropType<T> = Prop<T>;
export type PropDefaultValue<T> = T;


export type PropValidatorFunction<T> = (value: T) => boolean;
export type PropValidator<T> = PropOptions<T>;


export type PropOptions<T> = {
type: PropType<T>;

value?: PropDefaultValue<T>,
required?: boolean;
validator?: PropValidatorFunction<T>;
}

export type RecordPropsDefinition<T> = {
[K in keyof T]: PropValidator<T[K]>
}
export type PropsDefinition<T> = RecordPropsDefinition<T>;


declare function extend<T>({ props }: { props: PropsDefinition<T> }): PropsDefinition<T>;

interface MyType {
valid: boolean;
}

const r = extend({
props: {
notResolved: {
type: Object as PropType<MyType>,
validator: x => {
return x.valid;
}
},
explicit: {
type: Object as PropType<MyType>,
validator: (x: MyType) => {
return x.valid;
}
}
}
})

r.explicit
r.notResolved
r.explicit.required
r.notResolved.required

// Modified repro from #30505

type Box<T> = {
contents?: T;
contains?(content: T): boolean;
};

type Mapped<T> = {
[K in keyof T]: Box<T[K]>;
}

declare function id<T>(arg: Mapped<T>): Mapped<T>;

// All properties have inferable types

const obj1 = id({
foo: {
contents: ""
}
});

// Some properties have inferable types

const obj2 = id({
foo: {
contents: "",
contains(k) {
return k.length > 0;
}
}
});

// No properties have inferable types

const obj3 = id({
foo: {
contains(k) {
return k.length > 0;
}
}
});


//// [reverseMappedPartiallyInferableTypes.js]
"use strict";
// Repro from #30505
exports.__esModule = true;
var r = extend({
props: {
notResolved: {
type: Object,
validator: function (x) {
return x.valid;
}
},
explicit: {
type: Object,
validator: function (x) {
return x.valid;
}
}
}
});
r.explicit;
r.notResolved;
r.explicit.required;
r.notResolved.required;
// All properties have inferable types
var obj1 = id({
foo: {
contents: ""
}
});
// Some properties have inferable types
var obj2 = id({
foo: {
contents: "",
contains: function (k) {
return k.length > 0;
}
}
});
// No properties have inferable types
var obj3 = id({
foo: {
contains: function (k) {
return k.length > 0;
}
}
});

0 comments on commit ae3d1d4

Please sign in to comment.