Skip to content

Properly handle unions and intersections with keyof T and T[K] #12643

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 3 commits into from
Dec 4, 2016
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
23 changes: 10 additions & 13 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4523,7 +4523,7 @@ namespace ts {
// First, if the constraint type is a type parameter, obtain the base constraint. Then,
// if the key type is a 'keyof X', obtain 'keyof C' where C is the base constraint of X.
// Finally, iterate over the constituents of the resulting iteration type.
const keyType = constraintType.flags & TypeFlags.TypeParameter ? getApparentType(constraintType) : constraintType;
const keyType = constraintType.flags & TypeFlags.TypeVariable ? getApparentType(constraintType) : constraintType;
const iterationType = keyType.flags & TypeFlags.Index ? getIndexType(getApparentType((<IndexType>keyType).type)) : keyType;
forEachType(iterationType, t => {
// Create a mapper from T to the current iteration type constituent. Then, if the
Expand Down Expand Up @@ -4579,7 +4579,7 @@ namespace ts {
function isGenericMappedType(type: Type) {
if (getObjectFlags(type) & ObjectFlags.Mapped) {
const constraintType = getConstraintTypeFromMappedType(<MappedType>type);
return !!(constraintType.flags & (TypeFlags.TypeParameter | TypeFlags.Index));
return maybeTypeOfKind(constraintType, TypeFlags.TypeVariable | TypeFlags.Index);
}
return false;
}
Expand Down Expand Up @@ -5912,7 +5912,7 @@ namespace ts {
return links.resolvedType;
}

function getIndexTypeForTypeVariable(type: TypeVariable) {
function getIndexTypeForGenericType(type: TypeVariable | UnionOrIntersectionType) {
if (!type.resolvedIndexType) {
type.resolvedIndexType = <IndexType>createType(TypeFlags.Index);
type.resolvedIndexType.type = type;
Expand All @@ -5931,7 +5931,7 @@ namespace ts {
}

function getIndexType(type: Type): Type {
return type.flags & TypeFlags.TypeVariable ? getIndexTypeForTypeVariable(<TypeVariable>type) :
return maybeTypeOfKind(type, TypeFlags.TypeVariable) ? getIndexTypeForGenericType(<TypeVariable | UnionOrIntersectionType>type) :
getObjectFlags(type) & ObjectFlags.Mapped ? getConstraintTypeFromMappedType(<MappedType>type) :
type.flags & TypeFlags.Any || getIndexInfoOfType(type, IndexKind.String) ? stringType :
getLiteralTypeFromPropertyNames(type);
Expand Down Expand Up @@ -6032,14 +6032,11 @@ namespace ts {
}

function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
if (indexType.flags & TypeFlags.TypeVariable ||
objectType.flags & TypeFlags.TypeVariable && indexType.flags & TypeFlags.Index ||
isGenericMappedType(objectType)) {
// If the object type is a type variable (a type parameter or another indexed access type), if the
// index type is a type variable or an index type, or if the object type is a mapped type with a
// generic constraint, we are performing a higher-order index access where we cannot meaningfully
// access the properties of the object type. In those cases, we first check that the index type is
// assignable to 'keyof T' for the object type.
if (maybeTypeOfKind(indexType, TypeFlags.TypeVariable | TypeFlags.Index) || isGenericMappedType(objectType)) {
// If the index type is generic or if the object type is a mapped type with a generic constraint,
// we are performing a higher-order index access where we cannot meaningfully access the properties
// of the object type. In those cases, we first check that the index type is assignable to 'keyof T'
// for the object type.
if (accessNode) {
if (!isTypeAssignableTo(indexType, getIndexType(objectType))) {
error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType));
Expand Down Expand Up @@ -6539,7 +6536,7 @@ namespace ts {
// union type A | undefined, we produce { [P in keyof A]: X } | undefined.
const constraintType = getConstraintTypeFromMappedType(type);
if (constraintType.flags & TypeFlags.Index) {
const typeVariable = <TypeParameter>(<IndexType>constraintType).type;
const typeVariable = (<IndexType>constraintType).type;
const mappedTypeVariable = instantiateType(typeVariable, mapper);
if (typeVariable !== mappedTypeVariable) {
return mapType(mappedTypeVariable, t => {
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2908,6 +2908,8 @@ namespace ts {
/* @internal */
resolvedProperties: SymbolTable; // Cache of resolved properties
/* @internal */
resolvedIndexType: IndexType;
/* @internal */
couldContainTypeVariables: boolean;
}

Expand Down Expand Up @@ -2991,7 +2993,7 @@ namespace ts {

// keyof T types (TypeFlags.Index)
export interface IndexType extends Type {
type: TypeVariable;
type: TypeVariable | UnionOrIntersectionType;
}

export const enum SignatureKind {
Expand Down
119 changes: 118 additions & 1 deletion tests/baselines/reference/keyofAndIndexedAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,36 @@ function f60<T>(source: T, target: T) {
}
}

function f70(func: <T, U>(k1: keyof (T | U), k2: keyof (T & U)) => void) {
func<{ a: any, b: any }, { a: any, c: any }>('a', 'a');
func<{ a: any, b: any }, { a: any, c: any }>('a', 'b');
func<{ a: any, b: any }, { a: any, c: any }>('a', 'c');
}

function f71(func: <T, U>(x: T, y: U) => Partial<T & U>) {
let x = func({ a: 1, b: "hello" }, { c: true });
x.a; // number | undefined
x.b; // string | undefined
x.c; // boolean | undefined
}

function f72(func: <T, U, K extends keyof T | keyof U>(x: T, y: U, k: K) => (T & U)[K]) {
let a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
let b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
let c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
}

function f73(func: <T, U, K extends keyof (T & U)>(x: T, y: U, k: K) => (T & U)[K]) {
let a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
let b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
let c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
}

function f74(func: <T, U, K extends keyof (T | U)>(x: T, y: U, k: K) => (T | U)[K]) {
let a = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'a'); // number
let b = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'b'); // string | boolean
}

// Repros from #12011

class Base {
Expand Down Expand Up @@ -291,7 +321,39 @@ var empty = one(() => {}) // inferred as {}, expected
type Handlers<T> = { [K in keyof T]: (t: T[K]) => void }
declare function on<T>(handlerHash: Handlers<T>): T
var hashOfEmpty1 = on({ test: () => {} }); // {}
var hashOfEmpty2 = on({ test: (x: boolean) => {} }); // { test: boolean }
var hashOfEmpty2 = on({ test: (x: boolean) => {} }); // { test: boolean }

// Repro from #12624

interface Options1<Data, Computed> {
data?: Data
computed?: Computed;
}

declare class Component1<Data, Computed> {
constructor(options: Options1<Data, Computed>);
get<K extends keyof (Data & Computed)>(key: K): (Data & Computed)[K];
}

let c1 = new Component1({
data: {
hello: ""
}
});

c1.get("hello");

// Repro from #12625

interface Options2<Data, Computed> {
data?: Data
computed?: Computed;
}

declare class Component2<Data, Computed> {
constructor(options: Options2<Data, Computed>);
get<K extends keyof Data | keyof Computed>(key: K): (Data & Computed)[K];
}

//// [keyofAndIndexedAccess.js]
var __extends = (this && this.__extends) || function (d, b) {
Expand Down Expand Up @@ -438,6 +500,31 @@ function f60(source, target) {
target[k] = source[k];
}
}
function f70(func) {
func('a', 'a');
func('a', 'b');
func('a', 'c');
}
function f71(func) {
var x = func({ a: 1, b: "hello" }, { c: true });
x.a; // number | undefined
x.b; // string | undefined
x.c; // boolean | undefined
}
function f72(func) {
var a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
var b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
var c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
}
function f73(func) {
var a = func({ a: 1, b: "hello" }, { c: true }, 'a'); // number
var b = func({ a: 1, b: "hello" }, { c: true }, 'b'); // string
var c = func({ a: 1, b: "hello" }, { c: true }, 'c'); // boolean
}
function f74(func) {
var a = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'a'); // number
var b = func({ a: 1, b: "hello" }, { a: 2, b: true }, 'b'); // string | boolean
}
// Repros from #12011
var Base = (function () {
function Base() {
Expand Down Expand Up @@ -496,6 +583,12 @@ var assignTo2 = function (object, key1, key2) {
var empty = one(function () { }); // inferred as {}, expected
var hashOfEmpty1 = on({ test: function () { } }); // {}
var hashOfEmpty2 = on({ test: function (x) { } }); // { test: boolean }
var c1 = new Component1({
data: {
hello: ""
}
});
c1.get("hello");


//// [keyofAndIndexedAccess.d.ts]
Expand Down Expand Up @@ -603,6 +696,11 @@ declare function f53<T, K extends keyof T>(obj: {
declare function f54<T>(obj: T, key: keyof T): void;
declare function f55<T, K extends keyof T>(obj: T, key: K): void;
declare function f60<T>(source: T, target: T): void;
declare function f70(func: <T, U>(k1: keyof (T | U), k2: keyof (T & U)) => void): void;
declare function f71(func: <T, U>(x: T, y: U) => Partial<T & U>): void;
declare function f72(func: <T, U, K extends keyof T | keyof U>(x: T, y: U, k: K) => (T & U)[K]): void;
declare function f73(func: <T, U, K extends keyof (T & U)>(x: T, y: U, k: K) => (T & U)[K]): void;
declare function f74(func: <T, U, K extends keyof (T | U)>(x: T, y: U, k: K) => (T | U)[K]): void;
declare class Base {
get<K extends keyof this>(prop: K): this[K];
set<K extends keyof this>(prop: K, value: this[K]): void;
Expand Down Expand Up @@ -640,3 +738,22 @@ declare var hashOfEmpty1: {};
declare var hashOfEmpty2: {
test: boolean;
};
interface Options1<Data, Computed> {
data?: Data;
computed?: Computed;
}
declare class Component1<Data, Computed> {
constructor(options: Options1<Data, Computed>);
get<K extends keyof (Data & Computed)>(key: K): (Data & Computed)[K];
}
declare let c1: Component1<{
hello: string;
}, {}>;
interface Options2<Data, Computed> {
data?: Data;
computed?: Computed;
}
declare class Component2<Data, Computed> {
constructor(options: Options2<Data, Computed>);
get<K extends keyof Data | keyof Computed>(key: K): (Data & Computed)[K];
}
Loading