Skip to content

Inferred type parameter constraints #21709

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 9 commits into from
Feb 7, 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
147 changes: 98 additions & 49 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6963,6 +6963,42 @@ namespace ts {
return type.symbol && getDeclarationOfKind<TypeParameterDeclaration>(type.symbol, SyntaxKind.TypeParameter).constraint;
}

function getInferredTypeParameterConstraint(typeParameter: TypeParameter) {
let inferences: Type[];
if (typeParameter.symbol) {
for (const declaration of typeParameter.symbol.declarations) {
// When an 'infer T' declaration is immediately contained in a type reference node
// (such as 'Foo<infer T>'), T's constraint is inferred from the constraint of the
// corresponding type parameter in 'Foo'. When multiple 'infer T' declarations are
// present, we form an intersection of the inferred constraint types.
if (declaration.parent.kind === SyntaxKind.InferType && declaration.parent.parent.kind === SyntaxKind.TypeReference) {
const typeReference = <TypeReferenceNode>declaration.parent.parent;
const typeParameters = getTypeParametersForTypeReference(typeReference);
if (typeParameters) {
const index = typeReference.typeArguments.indexOf(<TypeNode>declaration.parent);
if (index < typeParameters.length) {
const declaredConstraint = getConstraintOfTypeParameter(typeParameters[index]);
if (declaredConstraint) {
// Type parameter constraints can reference other type parameters so
// constraints need to be instantiated. If instantiation produces the
// type parameter itself, we discard that inference. For example, in
// type Foo<T extends string, U extends T> = [T, U];
// type Bar<T> = T extends Foo<infer X, infer X> ? Foo<X, X> : T;
// the instantiated constraint for U is X, so we discard that inference.
const mapper = createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReference, typeParameters));
const constraint = instantiateType(declaredConstraint, mapper);
if (constraint !== typeParameter) {
inferences = append(inferences, constraint);
}
}
}
}
}
}
}
return inferences && getIntersectionType(inferences);
}

function getConstraintFromTypeParameter(typeParameter: TypeParameter): Type {
if (!typeParameter.constraint) {
if (typeParameter.target) {
Expand All @@ -6971,7 +7007,8 @@ namespace ts {
}
else {
const constraintDeclaration = getConstraintDeclaration(typeParameter);
typeParameter.constraint = constraintDeclaration ? getTypeFromTypeNode(constraintDeclaration) : noConstraintType;
typeParameter.constraint = constraintDeclaration ? getTypeFromTypeNode(constraintDeclaration) :
getInferredTypeParameterConstraint(typeParameter) || noConstraintType;
}
}
return typeParameter.constraint === noConstraintType ? undefined : typeParameter.constraint;
Expand Down Expand Up @@ -7078,12 +7115,8 @@ namespace ts {
const typeArguments = concatenate(type.outerTypeParameters, fillMissingTypeArguments(typeArgs, typeParameters, minTypeArgumentCount, isJs));
return createTypeReference(<GenericType>type, typeArguments);
}
if (node.typeArguments) {
error(node, Diagnostics.Type_0_is_not_generic, typeToString(type));
return unknownType;
return checkNoTypeArguments(node, symbol) ? type : unknownType;
}
return type;
}

function getTypeAliasInstantiation(symbol: Symbol, typeArguments: Type[]): Type {
const type = getDeclaredTypeOfSymbol(symbol);
Expand Down Expand Up @@ -7120,12 +7153,8 @@ namespace ts {
}
return getTypeAliasInstantiation(symbol, typeArguments);
}
if (node.typeArguments) {
error(node, Diagnostics.Type_0_is_not_generic, symbolToString(symbol));
return unknownType;
return checkNoTypeArguments(node, symbol) ? type : unknownType;
}
return type;
}

function getTypeReferenceName(node: TypeReferenceType): EntityNameOrEntityNameExpression | undefined {
switch (node.kind) {
Expand Down Expand Up @@ -7165,12 +7194,10 @@ namespace ts {

// Get type from reference to named type that cannot be generic (enum or type parameter)
const res = tryGetDeclaredTypeOfSymbol(symbol);
if (res !== undefined) {
if (typeArguments) {
error(node, Diagnostics.Type_0_is_not_generic, symbolToString(symbol));
return unknownType;
}
return res.flags & TypeFlags.TypeParameter ? getConstrainedTypeParameter(<TypeParameter>res, node) : res;
if (res) {
return checkNoTypeArguments(node, symbol) ?
res.flags & TypeFlags.TypeParameter ? getConstrainedTypeParameter(<TypeParameter>res, node) : res :
unknownType;
}

if (!(symbol.flags & SymbolFlags.Value && isJSDocTypeReference(node))) {
Expand Down Expand Up @@ -7234,39 +7261,58 @@ namespace ts {
return node.flags & NodeFlags.JSDoc && node.kind === SyntaxKind.TypeReference;
}

function checkNoTypeArguments(node: TypeReferenceType, symbol?: Symbol) {
if (node.typeArguments) {
error(node, Diagnostics.Type_0_is_not_generic, symbol ? symbolToString(symbol) : declarationNameToString((<TypeReferenceNode>node).typeName));
return false;
}
return true;
}

function getIntendedTypeFromJSDocTypeReference(node: TypeReferenceNode): Type {
if (isIdentifier(node.typeName)) {
if (node.typeName.escapedText === "Object") {
if (isJSDocIndexSignature(node)) {
const indexed = getTypeFromTypeNode(node.typeArguments[0]);
const target = getTypeFromTypeNode(node.typeArguments[1]);
const index = createIndexInfo(target, /*isReadonly*/ false);
return createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, indexed === stringType && index, indexed === numberType && index);
}
return anyType;
}
const typeArgs = node.typeArguments;
switch (node.typeName.escapedText) {
case "String":
checkNoTypeArguments(node);
return stringType;
case "Number":
checkNoTypeArguments(node);
return numberType;
case "Boolean":
checkNoTypeArguments(node);
return booleanType;
case "Void":
checkNoTypeArguments(node);
return voidType;
case "Undefined":
checkNoTypeArguments(node);
return undefinedType;
case "Null":
checkNoTypeArguments(node);
return nullType;
case "Function":
case "function":
checkNoTypeArguments(node);
return globalFunctionType;
case "Array":
case "array":
return !node.typeArguments || !node.typeArguments.length ? anyArrayType : undefined;
return !typeArgs || !typeArgs.length ? anyArrayType : undefined;
case "Promise":
case "promise":
return !node.typeArguments || !node.typeArguments.length ? createPromiseType(anyType) : undefined;
return !typeArgs || !typeArgs.length ? createPromiseType(anyType) : undefined;
case "Object":
if (typeArgs && typeArgs.length === 2) {
if (isJSDocIndexSignature(node)) {
const indexed = getTypeFromTypeNode(typeArgs[0]);
const target = getTypeFromTypeNode(typeArgs[1]);
const index = createIndexInfo(target, /*isReadonly*/ false);
return createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, indexed === stringType && index, indexed === numberType && index);
}
return anyType;
}
checkNoTypeArguments(node);
return anyType;
}
}
}
Expand Down Expand Up @@ -20159,57 +20205,60 @@ namespace ts {
checkDecorators(node);
}

function checkTypeArgumentConstraints(typeParameters: TypeParameter[], typeArgumentNodes: ReadonlyArray<TypeNode>): boolean {
const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters);
function getEffectiveTypeArguments(node: TypeReferenceNode | ExpressionWithTypeArguments, typeParameters: TypeParameter[]) {
return fillMissingTypeArguments(map(node.typeArguments, getTypeFromTypeNode), typeParameters,
getMinTypeArgumentCount(typeParameters), isInJavaScriptFile(node));
}

function checkTypeArgumentConstraints(node: TypeReferenceNode | ExpressionWithTypeArguments, typeParameters: TypeParameter[]): boolean {
let typeArguments: Type[];
let mapper: TypeMapper;
let result = true;
for (let i = 0; i < typeParameters.length; i++) {
const constraint = getConstraintOfTypeParameter(typeParameters[i]);
if (constraint) {
if (!typeArguments) {
typeArguments = fillMissingTypeArguments(map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, minTypeArgumentCount, isInJavaScriptFile(typeArgumentNodes[i]));
typeArguments = getEffectiveTypeArguments(node, typeParameters);
mapper = createTypeMapper(typeParameters, typeArguments);
}
const typeArgument = typeArguments[i];
result = result && checkTypeAssignableTo(
typeArgument,
instantiateType(constraint, mapper),
typeArgumentNodes[i],
node.typeArguments[i],
Diagnostics.Type_0_does_not_satisfy_the_constraint_1);
}
}
return result;
}

function getTypeParametersForTypeReference(node: TypeReferenceNode | ExpressionWithTypeArguments) {
const type = getTypeFromTypeReference(node);
if (type !== unknownType) {
const symbol = getNodeLinks(node).resolvedSymbol;
if (symbol) {
return symbol.flags & SymbolFlags.TypeAlias && getSymbolLinks(symbol).typeParameters ||
(getObjectFlags(type) & ObjectFlags.Reference ? (<TypeReference>type).target.localTypeParameters : undefined);
}
}
return undefined;
}

function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) {
checkGrammarTypeArguments(node, node.typeArguments);
if (node.kind === SyntaxKind.TypeReference && node.typeName.jsdocDotPos !== undefined && !isInJavaScriptFile(node) && !isInJSDoc(node)) {
grammarErrorAtPos(node, node.typeName.jsdocDotPos, 1, Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments);

}
const type = getTypeFromTypeReference(node);
if (type !== unknownType) {
if (node.typeArguments) {
// Do type argument local checks only if referenced type is successfully resolved
forEach(node.typeArguments, checkSourceElement);
if (produceDiagnostics) {
const symbol = getNodeLinks(node).resolvedSymbol;
if (!symbol) {
// There is no resolved symbol cached if the type resolved to a builtin
// via JSDoc type reference resolution (eg, Boolean became boolean), none
// of which are generic when they have no associated symbol
// (additionally, JSDoc's index signature syntax, Object<string, T> actually uses generic syntax without being generic)
if (!isJSDocIndexSignature(node)) {
error(node, Diagnostics.Type_0_is_not_generic, typeToString(type));
}
return;
}
let typeParameters = symbol.flags & SymbolFlags.TypeAlias && getSymbolLinks(symbol).typeParameters;
if (!typeParameters && getObjectFlags(type) & ObjectFlags.Reference) {
typeParameters = (<TypeReference>type).target.localTypeParameters;
const typeParameters = getTypeParametersForTypeReference(node);
if (typeParameters) {
checkTypeArgumentConstraints(node, typeParameters);
}
checkTypeArgumentConstraints(typeParameters, node.typeArguments);
}
}
if (type.flags & TypeFlags.Enum && getNodeLinks(node).resolvedSymbol.flags & SymbolFlags.EnumMember) {
Expand Down Expand Up @@ -22906,7 +22955,7 @@ namespace ts {
if (some(baseTypeNode.typeArguments)) {
forEach(baseTypeNode.typeArguments, checkSourceElement);
for (const constructor of getConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode)) {
if (!checkTypeArgumentConstraints(constructor.typeParameters, baseTypeNode.typeArguments)) {
if (!checkTypeArgumentConstraints(baseTypeNode, constructor.typeParameters)) {
break;
}
}
Expand Down
29 changes: 28 additions & 1 deletion tests/baselines/reference/inferTypes1.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ tests/cases/conformance/types/conditional/inferTypes1.ts(72,15): error TS2304: C
tests/cases/conformance/types/conditional/inferTypes1.ts(72,15): error TS4081: Exported type alias 'T62' has or is using private name 'U'.
tests/cases/conformance/types/conditional/inferTypes1.ts(72,43): error TS2304: Cannot find name 'U'.
tests/cases/conformance/types/conditional/inferTypes1.ts(72,43): error TS4081: Exported type alias 'T62' has or is using private name 'U'.
tests/cases/conformance/types/conditional/inferTypes1.ts(78,44): error TS2344: Type 'U' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'.


==== tests/cases/conformance/types/conditional/inferTypes1.ts (11 errors) ====
==== tests/cases/conformance/types/conditional/inferTypes1.ts (12 errors) ====
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
Expand Down Expand Up @@ -109,6 +111,22 @@ tests/cases/conformance/types/conditional/inferTypes1.ts(72,43): error TS4081: E
~
!!! error TS4081: Exported type alias 'T62' has or is using private name 'U'.

type T70<T extends string> = { x: T };
type T71<T> = T extends T70<infer U> ? T70<U> : never;

type T72<T extends number> = { y: T };
type T73<T> = T extends T72<infer U> ? T70<U> : never; // Error
~
!!! error TS2344: Type 'U' does not satisfy the constraint 'string'.
!!! error TS2344: Type 'number' is not assignable to type 'string'.

type T74<T extends number, U extends string> = { x: T, y: U };
type T75<T> = T extends T74<infer U, infer U> ? T70<U> | T72<U> | T74<U, U> : never;

type T76<T extends T[], U extends T> = { x: T };
type T77<T> = T extends T76<infer X, infer Y> ? T76<X, Y> : never;
type T78<T> = T extends T76<infer X, infer X> ? T76<X, X> : never;

// Example from #21496

type JsonifiedObject<T extends object> = { [K in keyof T]: Jsonified<T[K]> };
Expand Down Expand Up @@ -140,4 +158,13 @@ tests/cases/conformance/types/conditional/inferTypes1.ts(72,43): error TS4081: E
declare let ex: JsonifiedExample;
const z1: "correct" = ex.customClass;
const z2: string = ex.obj.nested.attr;

// Repros from #21631

type A1<T, U extends A1<any, any>> = [T, U];
type B1<S> = S extends A1<infer T, infer U> ? [T, U] : never;

type A2<T, U extends void> = [T, U];
type B2<S> = S extends A2<infer T, infer U> ? [T, U] : never;
type C2<S, U extends void> = S extends A2<infer T, U> ? [T, U] : never;

22 changes: 22 additions & 0 deletions tests/baselines/reference/inferTypes1.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ type T60 = infer U; // Error
type T61<T> = infer A extends infer B ? infer C : infer D; // Error
type T62<T> = U extends (infer U)[] ? U : U; // Error

type T70<T extends string> = { x: T };
type T71<T> = T extends T70<infer U> ? T70<U> : never;

type T72<T extends number> = { y: T };
type T73<T> = T extends T72<infer U> ? T70<U> : never; // Error

type T74<T extends number, U extends string> = { x: T, y: U };
type T75<T> = T extends T74<infer U, infer U> ? T70<U> | T72<U> | T74<U, U> : never;

type T76<T extends T[], U extends T> = { x: T };
type T77<T> = T extends T76<infer X, infer Y> ? T76<X, Y> : never;
type T78<T> = T extends T76<infer X, infer X> ? T76<X, X> : never;

// Example from #21496

type JsonifiedObject<T extends object> = { [K in keyof T]: Jsonified<T[K]> };
Expand Down Expand Up @@ -103,6 +116,15 @@ type JsonifiedExample = Jsonified<Example>;
declare let ex: JsonifiedExample;
const z1: "correct" = ex.customClass;
const z2: string = ex.obj.nested.attr;

// Repros from #21631

type A1<T, U extends A1<any, any>> = [T, U];
type B1<S> = S extends A1<infer T, infer U> ? [T, U] : never;

type A2<T, U extends void> = [T, U];
type B2<S> = S extends A2<infer T, infer U> ? [T, U] : never;
type C2<S, U extends void> = S extends A2<infer T, U> ? [T, U] : never;


//// [inferTypes1.js]
Expand Down
Loading