Skip to content
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

Fix indexed access in conditional type #22707

Merged
merged 7 commits into from
Mar 21, 2018
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
47 changes: 32 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3033,7 +3033,7 @@ namespace ts {
return createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode);
}
if (type.flags & TypeFlags.Substitution) {
return typeToTypeNodeHelper((<SubstitutionType>type).typeParameter, context);
return typeToTypeNodeHelper((<SubstitutionType>type).typeVariable, context);
}

Debug.fail("Should be unreachable.");
Expand Down Expand Up @@ -7303,7 +7303,7 @@ namespace ts {
const res = tryGetDeclaredTypeOfSymbol(symbol);
if (res) {
return checkNoTypeArguments(node, symbol) ?
res.flags & TypeFlags.TypeParameter ? getConstrainedTypeParameter(<TypeParameter>res, node) : res :
res.flags & TypeFlags.TypeParameter ? getConstrainedTypeVariable(<TypeParameter>res, node) : res :
unknownType;
}

Expand Down Expand Up @@ -7342,25 +7342,36 @@ namespace ts {
}
}

function getSubstitutionType(typeParameter: TypeParameter, substitute: Type) {
function getSubstitutionType(typeVariable: TypeVariable, substitute: Type) {
const result = <SubstitutionType>createType(TypeFlags.Substitution);
result.typeParameter = typeParameter;
result.typeVariable = typeVariable;
result.substitute = substitute;
return result;
}

function getConstrainedTypeParameter(typeParameter: TypeParameter, node: Node) {
function isUnaryTupleTypeNode(node: TypeNode) {
return node.kind === SyntaxKind.TupleType && (<TupleTypeNode>node).elementTypes.length === 1;
}

function getImpliedConstraint(typeVariable: TypeVariable, checkNode: TypeNode, extendsNode: TypeNode): Type {
return isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode) ? getImpliedConstraint(typeVariable, (<TupleTypeNode>checkNode).elementTypes[0], (<TupleTypeNode>extendsNode).elementTypes[0]) :
Copy link
Member

Choose a reason for hiding this comment

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

Nit: shorten this line

Copy link
Member

Choose a reason for hiding this comment

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

Also, if you made isUnaryTupleTypeNode a type predicate, you could potentially avoid the type assertions

getActualTypeVariable(getTypeFromTypeNode(checkNode)) === typeVariable ? getTypeFromTypeNode(extendsNode) :
undefined;
}

function getConstrainedTypeVariable(typeVariable: TypeVariable, node: Node) {
let constraints: Type[];
while (isPartOfTypeNode(node)) {
const parent = node.parent;
if (parent.kind === SyntaxKind.ConditionalType && node === (<ConditionalTypeNode>parent).trueType) {
if (getTypeFromTypeNode((<ConditionalTypeNode>parent).checkType) === typeParameter) {
constraints = append(constraints, getTypeFromTypeNode((<ConditionalTypeNode>parent).extendsType));
const constraint = getImpliedConstraint(typeVariable, (<ConditionalTypeNode>parent).checkType, (<ConditionalTypeNode>parent).extendsType);
if (constraint) {
constraints = append(constraints, constraint);
}
}
node = parent;
}
return constraints ? getSubstitutionType(typeParameter, getIntersectionType(append(constraints, typeParameter))) : typeParameter;
return constraints ? getSubstitutionType(typeVariable, getIntersectionType(append(constraints, typeVariable))) : typeVariable;
}

function isJSDocTypeReference(node: TypeReferenceType): node is TypeReferenceNode {
Expand Down Expand Up @@ -8256,7 +8267,13 @@ namespace ts {
function getTypeFromIndexedAccessTypeNode(node: IndexedAccessTypeNode) {
const links = getNodeLinks(node);
if (!links.resolvedType) {
links.resolvedType = getIndexedAccessType(getTypeFromTypeNode(node.objectType), getTypeFromTypeNode(node.indexType), node);
const objectType = getTypeFromTypeNode(node.objectType);
const indexType = getTypeFromTypeNode(node.indexType);
const resolved = getIndexedAccessType(objectType, indexType, node);
links.resolvedType = resolved.flags & TypeFlags.IndexedAccess &&
(<IndexedAccessType>resolved).objectType === objectType &&
(<IndexedAccessType>resolved).indexType === indexType ?
getConstrainedTypeVariable(<IndexedAccessType>resolved, node) : resolved;
}
return links.resolvedType;
}
Expand All @@ -8276,8 +8293,8 @@ namespace ts {
return links.resolvedType;
}

function getActualTypeParameter(type: Type) {
return type.flags & TypeFlags.Substitution ? (<SubstitutionType>type).typeParameter : type;
function getActualTypeVariable(type: Type) {
return type.flags & TypeFlags.Substitution ? (<SubstitutionType>type).typeVariable : type;
}

function getConditionalType(root: ConditionalRoot, mapper: TypeMapper): Type {
Expand Down Expand Up @@ -8321,7 +8338,7 @@ namespace ts {
}
}
// Return a deferred type for a check that is neither definitely true nor definitely false
const erasedCheckType = getActualTypeParameter(checkType);
const erasedCheckType = getActualTypeVariable(checkType);
const result = <ConditionalType>createType(TypeFlags.Conditional);
result.root = root;
result.checkType = erasedCheckType;
Expand Down Expand Up @@ -9041,7 +9058,7 @@ namespace ts {
return getConditionalTypeInstantiation(<ConditionalType>type, combineTypeMappers((<ConditionalType>type).mapper, mapper));
}
if (type.flags & TypeFlags.Substitution) {
return mapper((<SubstitutionType>type).typeParameter);
return instantiateType((<SubstitutionType>type).typeVariable, mapper);
}
}
return type;
Expand Down Expand Up @@ -9647,10 +9664,10 @@ namespace ts {
target = (<LiteralType>target).regularType;
}
if (source.flags & TypeFlags.Substitution) {
source = relation === definitelyAssignableRelation ? (<SubstitutionType>source).typeParameter : (<SubstitutionType>source).substitute;
source = relation === definitelyAssignableRelation ? (<SubstitutionType>source).typeVariable : (<SubstitutionType>source).substitute;
}
if (target.flags & TypeFlags.Substitution) {
target = (<SubstitutionType>target).typeParameter;
target = (<SubstitutionType>target).typeVariable;
}

// both types are the same - covers 'they are the same primitive type or both are Any' or the same type parameter cases
Expand Down
16 changes: 9 additions & 7 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3844,6 +3844,8 @@ namespace ts {
constraint?: Type;
}

export type TypeVariable = TypeParameter | IndexedAccessType;

// keyof T types (TypeFlags.Index)
export interface IndexType extends InstantiableType {
type: InstantiableType | UnionOrIntersectionType;
Expand Down Expand Up @@ -3875,14 +3877,14 @@ namespace ts {
}

// Type parameter substitution (TypeFlags.Substitution)
// Substitution types are created for type parameter references that occur in the true branch
// of a conditional type. For example, in 'T extends string ? Foo<T> : Bar<T>', the reference to
// T in Foo<T> is resolved as a substitution type that substitutes 'string & T' for T. Thus, if
// Foo has a 'string' constraint on its type parameter, T will satisfy it. Substitution types
// disappear upon instantiation (just like type parameters).
// Substitution types are created for type parameters or indexed access types that occur in the
// true branch of a conditional type. For example, in 'T extends string ? Foo<T> : Bar<T>', the
// reference to T in Foo<T> is resolved as a substitution type that substitutes 'string & T' for T.
// Thus, if Foo has a 'string' constraint on its type parameter, T will satisfy it. Substitution
// types disappear upon instantiation (just like type parameters).
export interface SubstitutionType extends InstantiableType {
typeParameter: TypeParameter; // Target type parameter
substitute: Type; // Type to substitute for type parameter
typeVariable: TypeVariable; // Target type variable
substitute: Type; // Type to substitute for type parameter
}

export const enum SignatureKind {
Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,7 @@ declare namespace ts {
indexType: Type;
constraint?: Type;
}
type TypeVariable = TypeParameter | IndexedAccessType;
interface IndexType extends InstantiableType {
type: InstantiableType | UnionOrIntersectionType;
}
Expand All @@ -2216,7 +2217,7 @@ declare namespace ts {
resolvedFalseType?: Type;
}
interface SubstitutionType extends InstantiableType {
typeParameter: TypeParameter;
typeVariable: TypeVariable;
substitute: Type;
}
enum SignatureKind {
Expand Down
3 changes: 2 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,7 @@ declare namespace ts {
indexType: Type;
constraint?: Type;
}
type TypeVariable = TypeParameter | IndexedAccessType;
interface IndexType extends InstantiableType {
type: InstantiableType | UnionOrIntersectionType;
}
Expand All @@ -2216,7 +2217,7 @@ declare namespace ts {
resolvedFalseType?: Type;
}
interface SubstitutionType extends InstantiableType {
typeParameter: TypeParameter;
typeVariable: TypeVariable;
substitute: Type;
}
enum SignatureKind {
Expand Down
20 changes: 18 additions & 2 deletions tests/baselines/reference/conditionalTypes1.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ tests/cases/conformance/types/conditional/conditionalTypes1.ts(159,5): error TS2
tests/cases/conformance/types/conditional/conditionalTypes1.ts(160,5): error TS2322: Type 'T' is not assignable to type 'ZeroOf<T>'.
Type 'string | number' is not assignable to type 'ZeroOf<T>'.
Type 'string' is not assignable to type 'ZeroOf<T>'.
tests/cases/conformance/types/conditional/conditionalTypes1.ts(250,9): error TS2403: Subsequent variable declarations must have the same type. Variable 'z' must be of type 'T1', but here has type 'Foo<T & U>'.
tests/cases/conformance/types/conditional/conditionalTypes1.ts(275,43): error TS2322: Type 'T95<U>' is not assignable to type 'T94<U>'.
tests/cases/conformance/types/conditional/conditionalTypes1.ts(255,9): error TS2403: Subsequent variable declarations must have the same type. Variable 'z' must be of type 'T1', but here has type 'Foo<T & U>'.
tests/cases/conformance/types/conditional/conditionalTypes1.ts(280,43): error TS2322: Type 'T95<U>' is not assignable to type 'T94<U>'.
Type 'boolean' is not assignable to type 'true'.


Expand Down Expand Up @@ -318,6 +318,11 @@ tests/cases/conformance/types/conditional/conditionalTypes1.ts(275,43): error TS
!!! error TS2322: Type 'string' is not assignable to type 'ZeroOf<T>'.
}

type T35<T extends { a: string, b: number }> = T[];
type T36<T> = T extends { a: string } ? T extends { b: number } ? T35<T> : never : never;
type T37<T> = T extends { b: number } ? T extends { a: string } ? T35<T> : never : never;
type T38<T> = [T] extends [{ a: string }] ? [T] extends [{ b: number }] ? T35<T> : never : never;

type Extends<T, U> = T extends U ? true : false;
type If<C extends boolean, T, F> = C extends true ? T : F;
type Not<C extends boolean> = If<C, false, true>;
Expand Down Expand Up @@ -477,4 +482,15 @@ tests/cases/conformance/types/conditional/conditionalTypes1.ts(275,43): error TS

type Test1 = NonFooKeys1<{foo: 1, bar: 2, baz: 3}>; // "bar" | "baz"
type Test2 = NonFooKeys2<{foo: 1, bar: 2, baz: 3}>; // "bar" | "baz"

// Repro from #21729

interface Foo2 { foo: string; }
interface Bar2 { bar: string; }
type FooBar = Foo2 | Bar2;
declare interface ExtractFooBar<FB extends FooBar> { }

type Extracted<Struct> = {
[K in keyof Struct]: Struct[K] extends FooBar ? ExtractFooBar<Struct[K]> : Struct[K];
}

47 changes: 47 additions & 0 deletions tests/baselines/reference/conditionalTypes1.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ function f21<T extends number | string>(x: T, y: ZeroOf<T>) {
y = x; // Error
}

type T35<T extends { a: string, b: number }> = T[];
type T36<T> = T extends { a: string } ? T extends { b: number } ? T35<T> : never : never;
type T37<T> = T extends { b: number } ? T extends { a: string } ? T35<T> : never : never;
type T38<T> = [T] extends [{ a: string }] ? [T] extends [{ b: number }] ? T35<T> : never : never;

type Extends<T, U> = T extends U ? true : false;
type If<C extends boolean, T, F> = C extends true ? T : F;
type Not<C extends boolean> = If<C, false, true>;
Expand Down Expand Up @@ -315,6 +320,17 @@ type NonFooKeys2<T extends object> = Exclude<keyof T, 'foo'>;

type Test1 = NonFooKeys1<{foo: 1, bar: 2, baz: 3}>; // "bar" | "baz"
type Test2 = NonFooKeys2<{foo: 1, bar: 2, baz: 3}>; // "bar" | "baz"

// Repro from #21729

interface Foo2 { foo: string; }
interface Bar2 { bar: string; }
type FooBar = Foo2 | Bar2;
declare interface ExtractFooBar<FB extends FooBar> { }

type Extracted<Struct> = {
[K in keyof Struct]: Struct[K] extends FooBar ? ExtractFooBar<Struct[K]> : Struct[K];
}


//// [conditionalTypes1.js]
Expand Down Expand Up @@ -517,6 +533,25 @@ declare type ZeroOf<T extends number | string | boolean> = T extends number ? 0
declare function zeroOf<T extends number | string | boolean>(value: T): ZeroOf<T>;
declare function f20<T extends string>(n: number, b: boolean, x: number | boolean, y: T): void;
declare function f21<T extends number | string>(x: T, y: ZeroOf<T>): void;
declare type T35<T extends {
a: string;
b: number;
}> = T[];
declare type T36<T> = T extends {
a: string;
} ? T extends {
b: number;
} ? T35<T> : never : never;
declare type T37<T> = T extends {
b: number;
} ? T extends {
a: string;
} ? T35<T> : never : never;
declare type T38<T> = [T] extends [{
a: string;
}] ? [T] extends [{
b: number;
}] ? T35<T> : never : never;
declare type Extends<T, U> = T extends U ? true : false;
declare type If<C extends boolean, T, F> = C extends true ? T : F;
declare type Not<C extends boolean> = If<C, false, true>;
Expand Down Expand Up @@ -624,3 +659,15 @@ declare type Test2 = NonFooKeys2<{
bar: 2;
baz: 3;
}>;
interface Foo2 {
foo: string;
}
interface Bar2 {
bar: string;
}
declare type FooBar = Foo2 | Bar2;
declare interface ExtractFooBar<FB extends FooBar> {
}
declare type Extracted<Struct> = {
[K in keyof Struct]: Struct[K] extends FooBar ? ExtractFooBar<Struct[K]> : Struct[K];
};
Loading