diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f50ee294fe7..f43bf197a05 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -673,6 +673,8 @@ namespace ts { const literalTypes = createMap(); const indexedAccessTypes = createMap(); const substitutionTypes = createMap(); + const rangeTypes = createMap(); + const inverseOffsetTypes = createMap(); const evolvingArrayTypes: EvolvingArrayType[] = []; const undefinedProperties = createMap() as UnderscoreEscapedMap; @@ -721,6 +723,7 @@ namespace ts { const nonPrimitiveType = createIntrinsicType(TypeFlags.NonPrimitive, "object"); const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]); const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType; + const offsetConstraintType = getUnionType([stringType, numberType]); const numberOrBigIntType = getUnionType([numberType, bigintType]); const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); @@ -876,6 +879,10 @@ namespace ts { const zeroType = getLiteralType(0); const zeroBigIntType = getLiteralType({ negative: false, base10Value: "0" }); + const boundarySymbol = createSymbol(SymbolFlags.None, InternalSymbolName.Boundary); + const lowerBoundType = createLiteralType(TypeFlags.NumberLiteral, 0, boundarySymbol); + const upperBoundType = getInverseOffsetType(lowerBoundType); + const resolutionTargets: TypeSystemEntity[] = []; const resolutionResults: boolean[] = []; const resolutionPropertyNames: TypeSystemPropertyName[] = []; @@ -982,6 +989,7 @@ namespace ts { } function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic { + if (message === Diagnostics.Type_0_cannot_be_used_to_index_type_1) debugger; const diagnostic = location ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) : createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); @@ -4218,6 +4226,12 @@ namespace ts { const indexTypeNode = typeToTypeNodeHelper(indexedType, context); return createTypeOperatorNode(indexTypeNode); } + if (type.flags & TypeFlags.InverseOffset) { + const indexType = (type).indexType; + context.approximateLength += 1; + const indexTypeNode = typeToTypeNodeHelper(indexType, context); + return createInverseOffsetTypeNode(indexTypeNode); + } if (type.flags & TypeFlags.IndexedAccess) { const objectTypeNode = typeToTypeNodeHelper((type).objectType, context); const indexTypeNode = typeToTypeNodeHelper((type).indexType, context); @@ -4235,6 +4249,12 @@ namespace ts { context.approximateLength += 15; return createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode); } + if (type.flags & TypeFlags.Range) { + const objectTypeNode = typeToTypeNodeHelper((type).objectType, context); + const startTypeNode = (type).startType !== lowerBoundType ? typeToTypeNodeHelper((type).startType, context) : undefined; + const endTypeNode = (type).endType !== upperBoundType ? typeToTypeNodeHelper((type).endType, context) : undefined; + return createRangeTypeNode(objectTypeNode, startTypeNode, endTypeNode); + } if (type.flags & TypeFlags.Substitution) { return typeToTypeNodeHelper((type).typeVariable, context); } @@ -9691,13 +9711,13 @@ namespace ts { else if ((type).objectFlags & ObjectFlags.ClassOrInterface) { resolveClassOrInterfaceMembers(type); } - else if ((type).objectFlags & ObjectFlags.ReverseMapped) { + else if ((type).objectFlags & ObjectFlags.ReverseMapped) { resolveReverseMappedTypeMembers(type as ReverseMappedType); } else if ((type).objectFlags & ObjectFlags.Anonymous) { resolveAnonymousTypeMembers(type); } - else if ((type).objectFlags & ObjectFlags.Mapped) { + else if ((type).objectFlags & ObjectFlags.Mapped) { resolveMappedTypeMembers(type); } } @@ -10007,6 +10027,9 @@ namespace ts { if (t.flags & TypeFlags.Index) { return keyofConstraintType; } + if (t.flags & TypeFlags.InverseOffset) { + return offsetConstraintType; + } if (t.flags & TypeFlags.IndexedAccess) { const baseObjectType = getBaseConstraint((t).objectType); const baseIndexType = getBaseConstraint((t).indexType); @@ -10020,6 +10043,13 @@ namespace ts { constraintDepth--; return result; } + if (t.flags & TypeFlags.Range) { + const baseObjectType = getBaseConstraint((t).objectType); + const baseStartType = getBaseConstraint((t).startType); + const baseEndType = getBaseConstraint((t).endType); + const baseRangeType = baseObjectType && baseStartType && baseEndType && getRangeTypeOrUndefined(baseObjectType, baseStartType, baseEndType); + return baseRangeType && getBaseConstraint(baseRangeType); + } if (t.flags & TypeFlags.Substitution) { return getBaseConstraint((t).substitute); } @@ -12218,6 +12248,7 @@ namespace ts { type === wildcardType ? wildcardType : type.flags & TypeFlags.Unknown ? neverType : type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType : + type.flags & TypeFlags.InverseOffset ? getIndexType(offsetConstraintType, stringsOnly, noIndexSignatures) : stringsOnly ? !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? stringType : getLiteralTypeFromProperties(type, TypeFlags.StringLiteral) : !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? getUnionType([stringType, numberType, getLiteralTypeFromProperties(type, TypeFlags.UniqueESSymbol)]) : getNonEnumNumberIndexInfo(type) ? getUnionType([numberType, getLiteralTypeFromProperties(type, TypeFlags.StringLiteral | TypeFlags.UniqueESSymbol)]) : @@ -12346,6 +12377,7 @@ namespace ts { if (objectType.flags & (TypeFlags.Any | TypeFlags.Never)) { return objectType; } + const stringIndexInfo = getIndexInfoOfType(objectType, IndexKind.String); const indexInfo = isTypeAssignableToKind(indexType, TypeFlags.NumberLike) && getIndexInfoOfType(objectType, IndexKind.Number) || stringIndexInfo; if (indexInfo) { @@ -12462,7 +12494,18 @@ namespace ts { } function isGenericIndexType(type: Type): boolean { - return maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive | TypeFlags.Index); + if (maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive | TypeFlags.Index)) { + return true; + } + if (type.flags & TypeFlags.UnionOrIntersection) { + for (const t of (type as UnionOrIntersectionType).types) { + if (t.flags & TypeFlags.InverseOffset && isGenericIndexType((t as InverseOffsetType).indexType)) { + return true; + } + } + return false; + } + return !!(type.flags & TypeFlags.InverseOffset) && isGenericIndexType((type as InverseOffsetType).indexType); } function isThisTypeParameter(type: Type): boolean { @@ -12471,6 +12514,7 @@ namespace ts { function getSimplifiedType(type: Type, writing: boolean): Type { return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type, writing) : + type.flags & TypeFlags.Range ? getSimplifiedRangeType(type, writing) : type.flags & TypeFlags.Conditional ? getSimplifiedConditionalType(type, writing) : type; } @@ -12542,6 +12586,65 @@ namespace ts { return type[cache] = type; } + function distributeRangeOverObjectType(objectType: Type, startType: Type, endType: Type, writing: boolean) { + // (T | U)[X:Y] -> T[X:Y] | U[X:Y] + // (T & U)[X:Y] -> T[X:Y] & U[X:Y] + if (objectType.flags & TypeFlags.UnionOrIntersection) { + const types = map((objectType as UnionOrIntersectionType).types, t => getSimplifiedType(getRangeType(t, startType, endType), writing)); + return objectType.flags & TypeFlags.Intersection ? getIntersectionType(types) : getUnionType(types); + } + } + + function distributeObjectOverRangeTypes(objectType: Type, startType: Type, endType: Type, writing: boolean) { + // T[A|B:C|D] -> T[A:C] | T[A:D] | T[B:C] | T[B:D] + if (startType.flags & TypeFlags.Union || endType.flags & TypeFlags.Union) { + const types: Type[] = []; + for (const start of getConstituents(startType)) { + for (const end of getConstituents(endType)) { + types.push(getSimplifiedType(getRangeType(objectType, start, end), writing)); + } + } + return getUnionType(types); + } + } + + function getSimplifiedRangeType(type: RangeType, writing: boolean) { + const cache = writing ? "simplifiedForWriting" : "simplifiedForReading"; + if (type[cache]) { + return type[cache] === circularConstraintType ? type : type[cache]!; + } + type[cache] = circularConstraintType; + // We recursively simplify the object type as it may in turn be a range type. For example, with + // '{ [P in T]: { [Q in U]: number } }[T:][U:]' we want to first simplify the inner range type. + const objectType = unwrapSubstitution(getSimplifiedType(type.objectType, writing)); + const startType = getSimplifiedType(type.startType, writing); + const endType = getSimplifiedType(type.endType, writing); + + // T[A|B:C|D] -> T[A:C] | T[A:D] | T[B:C] | T[B:D] + const distributedOverStartEnd = distributeObjectOverRangeTypes(objectType, startType, endType, writing); + if (distributedOverStartEnd) { + return type[cache] = distributedOverStartEnd; + } + + // Only do the inner distribution if the start/end offsets can no longer be instantiated to cause distribution again + if (!(startType.flags & TypeFlags.Instantiable) && !(endType.flags & TypeFlags.Instantiable)) { + // (T | U)[A:B] -> T[A:B] | U[A:B] + // (T & U)[A:B] -> T[A:B] & U[A:B] + const distributedOverObject = distributeRangeOverObjectType(objectType, startType, endType, writing); + if (distributedOverObject) { + return type[cache] = distributedOverObject; + } + } + + // So ultimately: + // ((A & B) | C)[K1 | K2:K3 | K4] + // -> ((A & B) | C)[K1:K3 | K4] | ((A & B) | C)[K2:K3 | K4] + // -> ((A & B) | C)[K1:K3] | ((A & B) | C)[K1:K4] | ((A & B) | C)[K2:K4] | ((A & B) | C)[K2:K4] + // -> (A & B)[K1:K3] | C[K1:K3] | (A & B)[K1:K4] | C[K1:K4] | (A & B)[K2:K3] | C[K2:K3] | (A & B)[K2:K4] | C[K2:K4] + // -> (A[K1:K3] & B[K1:K3]) | C[K1:K3] | (A[K1:K4] & B[K1:K4]) | C[K1:K4] | (A[K2:K3] & B[K2:K3]) | C[K2:K3] | (A[K2:K4] & B[K2:K4]) | C[K2:K4] + return type[cache] = type; + } + function getSimplifiedConditionalType(type: ConditionalType, writing: boolean) { const checkType = type.checkType; const extendsType = type.extendsType; @@ -12580,6 +12683,71 @@ namespace ts { return instantiateType(getTemplateTypeFromMappedType(objectType), templateMapper); } + function getInvertedIndexType(indexType: Type, objectType: Type, errorNode: Node | undefined) { + if (indexType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Number | TypeFlags.String)) { + return indexType; + } + let index = getNumericIndexFromIndexType(indexType); + if (index !== undefined) { + if (isArrayType(objectType)) { + return numberType; + } + if (isTupleType(objectType)) { + if (index < 0) { + return getLiteralType(-index); + } + const length = getLengthOfTupleType(objectType); + index = length - index; + if (index < 0) index = 0; + if (objectType.target.hasRestElement) { + if (index < length) { + const indexTypes: Type[] = []; + for (let i = index; i <= length; i++) { + indexTypes.push(getLiteralType(i)); + } + return getUnionType(indexTypes); + } + } + return getLiteralType(index); + } + } + if (errorNode) { + error(errorNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType)); + } + } + + function resolveInverseOffsets(indexType: Type, objectType: Type, errorNode: Node | undefined) { + if (indexType.flags & TypeFlags.Union) { + let indexTypes: Type[] | undefined; + let hasErrors = false; + const types = (indexType).types; + for (let i = 0; i < types.length; i++) { + const t = types[i]; + if (t.flags & TypeFlags.InverseOffset) { + if (!indexTypes) indexTypes = types.slice(0, i); + const inverted = getInvertedIndexType((t).indexType, objectType, errorNode); + if (inverted) { + indexTypes.push(inverted); + } + else if (!errorNode) { + return undefined; + } + else { + hasErrors = true; + } + } + else if (indexTypes) { + indexTypes.push(t); + } + } + return hasErrors ? undefined : indexTypes ? getUnionType(indexTypes) : indexType; + } + if (indexType.flags & TypeFlags.InverseOffset) { + return getInvertedIndexType((indexType).indexType, objectType, errorNode); + } + return indexType; + } + function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression): Type { return getIndexedAccessTypeOrUndefined(objectType, indexType, accessNode, AccessFlags.None) || (accessNode ? errorType : unknownType); } @@ -12603,13 +12771,18 @@ namespace ts { return objectType; } // Defer the operation by creating an indexed access type. - const id = objectType.id + "," + indexType.id; + const id = `${objectType.id},${indexType.id}`; let type = indexedAccessTypes.get(id); if (!type) { indexedAccessTypes.set(id, type = createIndexedAccessType(objectType, indexType)); } return type; } + const resolvedIndexType = resolveInverseOffsets(indexType, objectType, accessNode); + if (!resolvedIndexType) { + return undefined; + } + indexType = resolvedIndexType; // In the following we resolve T[K] to the type of the property in T selected by K. // We treat boolean as different from other unions to improve errors; // skipping straight to getPropertyTypeForIndexType gives errors with 'boolean' instead of 'true'. @@ -12668,6 +12841,315 @@ namespace ts { return links.resolvedType; } + function getRangeType(objectType: Type, startType: Type, endType: Type, rangeNode?: RangeTypeNode, accessFlags?: AccessFlags): Type { + return getRangeTypeOrUndefined(objectType, startType, endType, rangeNode, accessFlags) ?? (rangeNode ? errorType : unknownType); + } + + function getRangeTypeOrUndefined(objectType: Type, startType: Type, endType: Type, rangeNode?: RangeTypeNode, accessFlags = AccessFlags.None): Type | undefined { + if (objectType === wildcardType || startType === wildcardType || endType === wildcardType) { + return wildcardType; + } + // The object type is constrained to be an array or tuple type + if (!isTypeAssignableTo(objectType, anyReadonlyArrayType)) { + if (rangeNode) { + error(rangeNode, Diagnostics.Type_0_is_not_an_array_type, typeToString(objectType)); + } + return undefined; + } + // The start type is constrained to be a string- or number-like type + if (!isTypeAssignableTo(startType, offsetConstraintType)) { + if (rangeNode?.startType) { + error(rangeNode.startType, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(startType), typeToString(objectType)); + } + return undefined; + } + // The end type is constrained to be a string- or number-like type + if (!isTypeAssignableTo(endType, offsetConstraintType)) { + if (rangeNode?.endType) { + error(rangeNode.endType, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(endType), typeToString(objectType)); + } + return undefined; + } + // If the object, start, or end type is generic, we are performing a higher-order slice, and the operation + // is deferred until it can be instantiated. + if (isGenericObjectType(objectType) || isGenericIndexType(startType) || isGenericIndexType(endType)) { + // A generic start or end on 'any' or 'unkown' is 'any' or 'unknown' (respectively). + if (objectType.flags & TypeFlags.AnyOrUnknown) { + return objectType; + } + + // If we can eagerly resolve start and end and they indicate the range is either + // all inclusive or empty, we return the object type or an empty tuple (respectively). + let start = getNumericIndexFromIndexType(startType); + let end = getNumericIndexFromIndexType(endType); + if (start !== undefined && end !== undefined) { + // A range consisting of the entire array is just the array: + // T[:] -> T + // T[0:^0] -> T + if (start === 0 && end === ~0) { + return objectType; + } + // A range that starts at the upper bound (^0), or ends at the lower bound (0), is just an empty tuple: + // T[^0:] -> [] + // T[:0] -> [] + // A range that starts at or after it ends is just an empty tuple: + // T[2:1] -> [] + // T[1:1] -> [] + // T[^1:^2] -> [] + // T[^1:^1] -> [] + if (start === ~0 || end === 0 || + (start < 0 === end < 0) && start >= end) { + return createTupleType(emptyArray, 0, /*hasRestElement*/ false, !isMutableArrayOrTuple(objectType)); + } + } + + // Defer the operation until it can be fully instantiated. + const id = `${objectType.id},${startType.id},${endType.id}`; + let type = rangeTypes.get(id); + if (!type) { + type = createType(TypeFlags.Range); + type.objectType = objectType; + type.startType = startType; + type.endType = endType; + rangeTypes.set(id, type); + } + return type; + } + if (objectType.flags & TypeFlags.Union || startType.flags & TypeFlags.Union || endType.flags & TypeFlags.Union) { + const results: Type[] = []; + let hasErrors = false; + for (const object of getConstituents(objectType)) { + for (const start of getConstituents(startType)) { + for (const end of getConstituents(endType)) { + const type = getTupleSliceFromRangeType(object, objectType, start, end, rangeNode); + if (type) { + results.push(type); + } + else if (!rangeNode) { + // If there's no error node, we can immediately stop, since error reporting is off + return undefined; + } + else { + // Otherwise we set a flag and return at the end of the loop so we still mark all errors + hasErrors = true; + } + } + } + } + if (hasErrors) { + return undefined; + } + return accessFlags & AccessFlags.Writing ? getIntersectionType(results) : getUnionType(results); + } + return getTupleSliceFromRangeType(objectType, objectType, startType, endType, rangeNode); + } + + function getTupleSliceFromRangeType(objectType: Type, fullObjectType: Type, startType: Type, endType: Type, rangeNode: RangeTypeNode | undefined): Type | undefined { + Debug.assert(!(objectType.flags & TypeFlags.Union)); + Debug.assert(!(startType.flags & TypeFlags.Union)); + Debug.assert(!(endType.flags & TypeFlags.Union)); + + // If the start type is `string`, `number`, `any`, or `never`, the result is an array + // of a union of all element types from the lower bound (0) to end. + // + // If the end type is `string`, `number`, `any`, or `never`, the result is an array + // of a union of all element types from start to the upper bound (^0). + // + // T[number:number] -> T[number][] + // T[number:Y] -> T[0:Y][number][] + // T[X:number] -> T[X:^0][number][] + const startIndexType = startType.flags & TypeFlags.InverseOffset ? (startType).indexType : startType; + const endIndexType = endType.flags & TypeFlags.InverseOffset ? (endType).indexType : endType; + const hasNonLiteralStart = startIndexType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Number | TypeFlags.String); + const hasNonLiteralEnd = endIndexType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Number | TypeFlags.String); + if (hasNonLiteralStart && hasNonLiteralEnd) { + const accessType = getIndexedAccessType(objectType, numberType); + return createArrayType(accessType, !isMutableArrayOrTuple(objectType)); + } + if (hasNonLiteralStart) { + const limitedRange = getTupleSliceFromRangeType(objectType, fullObjectType, lowerBoundType, endType, rangeNode); + if (!limitedRange) { + return undefined; + } + const accessType = getIndexedAccessType(limitedRange, numberType); + return createArrayType(accessType, !isMutableArrayOrTuple(objectType)); + } + if (hasNonLiteralEnd) { + const limitedRange = getTupleSliceFromRangeType(objectType, fullObjectType, startType, upperBoundType, rangeNode); + if (!limitedRange) { + return undefined; + } + const accessType = getIndexedAccessType(limitedRange, numberType); + return createArrayType(accessType, !isMutableArrayOrTuple(objectType)); + } + + let start = getNumericIndexFromIndexType(startType); + if (start === undefined) { + if (rangeNode) { + error(rangeNode.startType ?? rangeNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(startType), typeToString(fullObjectType)); + } + return undefined; + } + + let end = getNumericIndexFromIndexType(endType); + if (end === undefined) { + if (rangeNode) { + error(rangeNode.endType ?? rangeNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(endType), typeToString(fullObjectType)); + } + return undefined; + } + + // A range consisting of the entire array is just the array: + // T[:] -> T + // T[0:^0] -> T + if (start === 0 && end === ~0) { + return objectType; + } + + // A range that starts at the upper bound (`^0`) or ends at the lower bound (`0`) is just an empty tuple: + // T[^0:] -> [] + // T[:0] -> [] + // A range that starts at or after it ends is just an empty tuple: + // T[2:1] -> [] + // T[1:1] -> [] + // T[^1:^2] -> [] + // T[^1:^1] -> [] + if (start === ~0 || end === 0 || + (start < 0 === end < 0) && start >= end) { + return createTupleType(emptyArray, 0, /*hasRestElement*/ false, !isMutableArrayOrTuple(objectType)); + } + + const elementTypes: Type[] = []; + let minLength = 0; + let hasRestElement = false; + let associatedNames: __String[] | undefined; + if (!isTupleType(objectType)) { + if (start < 0 === end < 0) { + // If both signs agree, we can collect a tuple of a fixed length: + // T[][0:1] -> [T?] + // T[][^1:^0] -> [T?] + const elementType = getElementTypeOfArrayType(objectType); + Debug.assert(elementType !== undefined); + const length = end - start; + for (let i = 0; i < length; i++) { + elementTypes.push(elementType); + } + } + else if (start < 0 && end > 0) { + // If start is inverse and end is not, we can create a tuple of min(abs(start), end) optional elements. + const elementType = getElementTypeOfArrayType(objectType); + Debug.assert(elementType !== undefined); + const length = Math.min(end, ~start); + for (let i = 0; i < length; i++) { + elementTypes.push(elementType); + } + } + else { + // If the signs disagree, we cannot determine the lower and upper bound, thus we should return the entire array: + // T[][0:^1] -> T[] + // T[][^1:10] -> T[] + return objectType; + } + } + else { + const length = getLengthOfTupleType(objectType); + if (objectType.target.hasRestElement) { + if (start < 0) { + // If our start position is inverted, the result is an array of all of the optional or rest element types + // along with the set of required element types starting from the right equal to start: + // [A, B, ...C[]][^0:] -> C[] + // [A, B, ...C[]][^1:] -> (B | C)[] + // [A, B, ...C[]][^2:] -> (A | B | C)[] + const types: Type[] = []; + for (let i = objectType.target.minLength; i <= length; i++) { + const elementType = getTupleElementType(objectType, i); + Debug.assert(elementType !== undefined); + types.push(elementType); + } + for (let i = 0; i < ~start && i < objectType.target.minLength; i++) { + const elementType = getTupleElementType(objectType, objectType.target.minLength - i); + Debug.assert(elementType !== undefined); + types.push(elementType); + } + return createArrayType(getUnionType(types), !isMutableArrayOrTuple(objectType)); + } + if (end < 0) { + // If our end position is inverted, the result is a tuple whose minimum length + // is reduced by the inverted amount: + // [A, B, C, ...D[]][1:^0] -> [B, C, ...D[]] + // [A, B, C, ...D[]][1:^1] -> [B, C?, ...D[]] + // [A, B, C, ...D[]][1:^2] -> [B?, C?, ...D[]] + hasRestElement = true; + minLength = -Math.min(~end, length); + end = length; + } + } + else { + // If we do not have a rest element, then we can explicitly adjust inverted offsets + // relative to the length of the tuple: + // [A, B, C][:^2] -> [A, B, C][:1] -> [A] + // [A, B, C][^2:] -> [A, B, C][1:] -> [B, C] + if (start < 0) start = Math.max(0, length - ~start); + if (end < 0) end = Math.max(0, length - ~end); + // We also clamp start and end to the length of the fixed tuple: + // [A, B][1:3] -> [A, B][1:2] -> [B] + if (start > length) start = length; + if (end > length) end = length; + } + + // At this point our offsets should no longer be inverted. + Debug.assert(start >= 0 && end >= 0); + + if (objectType.target.associatedNames) { + associatedNames = []; + } + + for (let i = start; i < end; i++) { + if (i < objectType.target.minLength) minLength++; + const elementType = getTupleElementType(objectType, i); + Debug.assert(elementType !== undefined); + elementTypes.push(elementType); + if (objectType.target.associatedNames && associatedNames && i < objectType.target.associatedNames.length) { + associatedNames.push(objectType.target.associatedNames[i]); + } + } + + if (minLength < 0) minLength = 0; + + if (hasRestElement) { + const restType = getTupleElementType(objectType, end); + Debug.assert(restType !== undefined); + if (objectType.target.associatedNames && associatedNames && end < objectType.target.associatedNames.length) { + associatedNames.push(objectType.target.associatedNames[end]); + } + elementTypes.push(restType); + } + } + + return createTupleType(elementTypes, minLength, hasRestElement, !isMutableArrayOrTuple(objectType), associatedNames); + } + + function getTypeFromRangeTypeNode(node: RangeTypeNode): Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const objectType = getTypeFromTypeNode(node.objectType); + // A missing start is the lower bound (0): + // T[:Y] -> T[0:Y] + const startType = node.startType ? getTypeFromTypeNode(node.startType) : lowerBoundType; + // A missing end is the upper bound (^0): + // T[X:] -> T[X:^0] + const endOffset = node.endType ? getTypeFromTypeNode(node.endType) : upperBoundType; + const resolved = getRangeType(objectType, startType, endOffset, node); + links.resolvedType = resolved.flags & TypeFlags.Range && + (resolved).objectType === objectType && + (resolved).startType === startType && + (resolved).endType === endOffset ? + getConstrainedTypeVariable(resolved, node) : + resolved; + } + return links.resolvedType; + } + function getActualTypeVariable(type: Type): Type { if (type.flags & TypeFlags.Substitution) { return (type).typeVariable; @@ -12677,6 +13159,15 @@ namespace ts { (type).indexType.flags & TypeFlags.Substitution)) { return getIndexedAccessType(getActualTypeVariable((type).objectType), getActualTypeVariable((type).indexType)); } + if (type.flags & TypeFlags.Range && ( + (type).objectType.flags & TypeFlags.Substitution || + (type).startType.flags & TypeFlags.Substitution || + (type).endType.flags & TypeFlags.Substitution)) { + return getRangeType( + getActualTypeVariable((type).objectType), + getActualTypeVariable((type).startType), + getActualTypeVariable((type).endType)); + } return type; } @@ -12804,6 +13295,71 @@ namespace ts { return links.resolvedType; } + /** + * Determines if `type` is a string or numeric literal type for an numeric index. If `type` was + * an inverted offset, the twos-complement of the index is returned. + * @param invertNegativeOffsets Indicates whether negative offsets from a non-inverted offset type + * should be treated as an inverted offset (default `true`). For an indexed access type, negative offsets should not + * be inverted: + * ``` + * // indexed access + * T[-1] -> T["-1"] + * + * // range + * T[-1:] -> T[^1] + * ``` + */ + function getNumericIndexFromIndexType(type: Type, invertNegativeOffsets = true) { + const indexType = type.flags & TypeFlags.InverseOffset ? (type as InverseOffsetType).indexType : type; + let index = + isNumericLiteralType(indexType) ? indexType.value : + isStringLiteralType(indexType) ? canonicalNumericIndex(indexType.value) : + undefined; + if (index === undefined) { + return undefined; + } + if (!isInteger(index)) { + return undefined; + } + let inverted = indexType !== type; + if (index < 0 && (inverted || invertNegativeOffsets)) { + index = -index; + inverted = !inverted; + } + return inverted ? ~index : index; + } + + function getInverseOffsetType(indexType: Type): Type { + if (indexType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Number | TypeFlags.String)) { + return indexType; + } + // The inverse offset of an inverse offset is the normal offset. + if (indexType.flags & TypeFlags.InverseOffset) { + return (indexType).indexType; + } + if (indexType.flags & TypeFlags.Union) { + return mapType(indexType, getInverseOffsetType); + } + // An inverse offset cannot be otherwise resolved without some context object from which to derive + // the length. + const id = "" + indexType.id; + let type = inverseOffsetTypes.get(id); + if (!type) { + type = createType(TypeFlags.InverseOffset); + type.indexType = indexType; + inverseOffsetTypes.set(id, type); + } + return type; + } + + function getTypeFromInverseOffsetTypeNode(node: InverseOffsetTypeNode): Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getInverseOffsetType(getTypeFromTypeNode(node.indexType)); + } + return links.resolvedType; + } + function getIdentifierChain(node: EntityName): Identifier[] { if (isIdentifier(node)) { return [node]; @@ -13292,10 +13848,14 @@ namespace ts { return getTypeFromIndexedAccessTypeNode(node); case SyntaxKind.MappedType: return getTypeFromMappedTypeNode(node); + case SyntaxKind.RangeType: + return getTypeFromRangeTypeNode(node); case SyntaxKind.ConditionalType: return getTypeFromConditionalTypeNode(node); case SyntaxKind.InferType: return getTypeFromInferTypeNode(node); + case SyntaxKind.InverseOffsetType: + return getTypeFromInverseOffsetTypeNode(node); case SyntaxKind.ImportType: return getTypeFromImportTypeNode(node); // This function assumes that an identifier or qualified name is a type expression @@ -13749,6 +14309,15 @@ namespace ts { if (flags & TypeFlags.Conditional) { return getConditionalTypeInstantiation(type, combineTypeMappers((type).mapper, mapper)); } + if (flags & TypeFlags.InverseOffset) { + return getInverseOffsetType(instantiateType((type).indexType, mapper)); + } + if (flags & TypeFlags.Range) { + return getRangeType( + instantiateType((type).objectType, mapper), + instantiateType((type).startType, mapper), + instantiateType((type).endType, mapper)); + } if (flags & TypeFlags.Substitution) { const maybeVariable = instantiateType((type).typeVariable, mapper); if (maybeVariable.flags & TypeFlags.TypeVariable) { @@ -15545,6 +16114,9 @@ namespace ts { if (flags & TypeFlags.Index) { return isRelatedTo((source).type, (target).type, /*reportErrors*/ false); } + if (flags & TypeFlags.InverseOffset) { + return isRelatedTo((source).indexType, (target).indexType, /*reportErrors*/ false); + } let result = Ternary.False; if (flags & TypeFlags.IndexedAccess) { if (result = isRelatedTo((source).objectType, (target).objectType, /*reportErrors*/ false)) { @@ -15642,6 +16214,39 @@ namespace ts { } } } + else if (target.flags & TypeFlags.InverseOffset) { + // A ^S is related to a ^T if S is related to T + if (source.flags & TypeFlags.InverseOffset) { + if (result = isRelatedTo((source).indexType, (target).indexType, /*reportErrors*/ false)) { + return result; + } + } + // TODO(rbuckton): Are they any other possible assignability relations? + } + else if (target.flags & TypeFlags.Range) { + // A type S is related to a type T[X:Y] if S is related to C, where C is the base + // constraint of T[X:Y] for writing. + if (relation !== identityRelation) { + const objectType = (target).objectType; + const startType = (target).startType; + const endType = (target).endType; + const baseObjectType = getBaseConstraintOfType(objectType) ?? objectType; + const baseStartType = getBaseConstraintOfType(startType) ?? startType; + const baseEndType = getBaseConstraintOfType(endType) ?? endType; + if (!isGenericObjectType(baseObjectType) && !isGenericIndexType(baseStartType) && !isGenericIndexType(baseEndType)) { + const accessFlags = AccessFlags.Writing | (baseObjectType !== objectType ? AccessFlags.NoIndexSignatures : 0); + const constraint = getRangeTypeOrUndefined( + baseObjectType, + baseStartType, + baseEndType, + /*rangeNode*/ undefined, + accessFlags); + if (constraint && (result = isRelatedTo(source, constraint, reportErrors))) { + return result; + } + } + } + } else if (isGenericMappedType(target)) { // A source type T is related to a target type { [P in X]: T[P] } const template = getTemplateTypeFromMappedType(target); @@ -15686,6 +16291,18 @@ namespace ts { return result; } } + if (source.flags & TypeFlags.Range && target.flags & TypeFlags.Range) { + // A type S[XS:YS] is related to a type T[XT:YT] if S is related to T, XS is related to XT, and YX is related to YT. + if (result = isRelatedTo((source).objectType, (target).objectType, reportErrors)) { + if (result &= isRelatedTo((source).startType, (target).startType, reportErrors)) { + result &= isRelatedTo((source).endType, (target).endType, reportErrors); + } + } + if (result) { + resetErrorInfo(saveErrorInfo); + return result; + } + } else { const constraint = getConstraintOfType(source); if (!constraint || (source.flags & TypeFlags.TypeParameter && constraint.flags & TypeFlags.Any)) { @@ -15713,6 +16330,12 @@ namespace ts { return result; } } + else if (source.flags & TypeFlags.InverseOffset) { + if (result = isRelatedTo(numberType, target, reportErrors)) { + resetErrorInfo(saveErrorInfo); + return result; + } + } else if (source.flags & TypeFlags.Conditional) { if (target.flags & TypeFlags.Conditional) { // Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if @@ -17038,6 +17661,14 @@ namespace ts { isUnitType(type); } + function isNumericLiteralType(type: Type): type is LiteralType & { value: number } { + return !!(type.flags & TypeFlags.NumberLiteral); + } + + function isStringLiteralType(type: Type): type is LiteralType & { value: string } { + return !!(type.flags & TypeFlags.StringLiteral); + } + function getBaseTypeOfLiteralType(type: Type): Type { return type.flags & TypeFlags.EnumLiteral ? getBaseTypeOfEnumLiteralType(type) : type.flags & TypeFlags.StringLiteral ? stringType : @@ -19071,6 +19702,10 @@ namespace ts { return type.flags & TypeFlags.Union ? (type as UnionType).types.length : 1; } + function getConstituents(type: Type) { + return type.flags & TypeFlags.Union ? (type as UnionType).types : [type]; + } + // Apply a mapping function to a type and return the resulting type. If the source type // is a union type, the mapping function is applied to each constituent type and a union // of the resulting types is returned. @@ -22285,7 +22920,7 @@ namespace ts { // Note that this accepts the values 'Infinity', '-Infinity', and 'NaN', and that this is intentional. // This is desired behavior, because when indexing with them as numeric entities, you are indexing // with the strings '"Infinity"', '"-Infinity"', and '"NaN"' respectively. - return (+name).toString() === name; + return canonicalNumericIndex(name) !== undefined; } function checkComputedPropertyName(node: ComputedPropertyName): Type { @@ -29162,6 +29797,43 @@ namespace ts { checkIndexedAccessIndexType(getTypeFromIndexedAccessTypeNode(node), node); } + function checkOffsetIndexType(offsetType: Type, offsetNode: InverseOffsetTypeNode) { + if (!(offsetType.flags & TypeFlags.InverseOffset)) { + return offsetType; + } + if (!checkTypeAssignableTo((offsetType).indexType, offsetConstraintType, offsetNode.indexType, Diagnostics.Type_0_does_not_satisfy_the_constraint_1)) { + return errorType; + } + let hasErrors = false; + forEachType((offsetType).indexType, t => { + if (isLiteralType(t) && getNumericIndexFromIndexType(t) === undefined) { + hasErrors = true; + if (offsetNode) { + error(offsetNode.indexType, Diagnostics.Type_0_cannot_be_used_as_an_index_type_in_an_offset_or_range_type, typeToString(t)); + } + else { + return true; + } + } + }); + if (hasErrors) { + return errorType; + } + return offsetType; + } + + function checkOffsetType(node: InverseOffsetTypeNode) { + checkSourceElement(node.indexType); + checkOffsetIndexType(getTypeFromInverseOffsetTypeNode(node), node); + } + + function checkRangeType(node: RangeTypeNode) { + checkSourceElement(node.objectType); + checkSourceElement(node.startType); + checkSourceElement(node.endType); + getTypeFromRangeTypeNode(node); + } + function checkMappedType(node: MappedTypeNode) { checkSourceElement(node.typeParameter); checkSourceElement(node.type); @@ -33671,6 +34343,10 @@ namespace ts { return checkSourceElement((node as JSDocTypeExpression).type); case SyntaxKind.IndexedAccessType: return checkIndexedAccessType(node); + case SyntaxKind.InverseOffsetType: + return checkOffsetType(node); + case SyntaxKind.RangeType: + return checkRangeType(node); case SyntaxKind.MappedType: return checkMappedType(node); case SyntaxKind.FunctionDeclaration: diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 4f9b7bc5798..0acad69ea5b 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1351,11 +1351,21 @@ namespace ts { } } + let lazyIsArray = (value: any): boolean => { + if (typeof Array.isArray === "function") { + lazyIsArray = Array.isArray; + } + else { + lazyIsArray = value => value instanceof Array; + } + return lazyIsArray(value); + } + /** * Tests whether a value is an array. */ export function isArray(value: any): value is readonly {}[] { - return Array.isArray ? Array.isArray(value) : value instanceof Array; + return lazyIsArray(value); } export function toArray(value: T | T[]): T[]; @@ -1367,13 +1377,63 @@ namespace ts { /** * Tests whether a value is string */ - export function isString(text: unknown): text is string { - return typeof text === "string"; + export function isString(x: unknown): x is string { + return typeof x === "string"; } + export function isNumber(x: unknown): x is number { return typeof x === "number"; } + let lazyIsInteger = (x: number): boolean => { + if (typeof Number.isInteger === "function") { + lazyIsInteger = Number.isInteger; + } + else { + // https://tc39.es/ecma262/#sec-isinteger + lazyIsInteger = x => + typeof x === "number" && + !isNaN(x) && + isFinite(x) && + Math.floor(Math.abs(x)) === Math.abs(x); + } + return lazyIsInteger(x); + } + + /** + * Tests whether the provided value is an integer. + * + * This emulates the behavior of [IsInteger (ECMA-262)](https://tc39.es/ecma262/#sec-isinteger). + */ + export function isInteger(x: number): boolean { + return lazyIsInteger(x); + } + + /** + * Returns the numeric value of a string, if that numeric value's string representation is equal + * to the source string; otherwise, returns `undefined`. + * + * This determines whether a numeric string is a valid numeric index (e.g., the string representation + * can be round-tripped through `ToString(ToNumber(x))`). + * + * For example: + * + * - `"1"` returns `1`, because: + * - `ToNumber("1") === 1` + * - `ToString(1) === "1"` + * - `"1.0"` returns `undefined`, because: + * - `ToNumber("1.0") === 1` + * - `ToString(1) !== "1.0"`. + * + * This emulates the behavior of [CanonicalNumericIndexString (ECMA-262)](https://tc39.es/ecma262/#sec-canonicalnumericindexstring). + */ + export function canonicalNumericIndex(x: string | __String): number | undefined { + if (x === "-0") return -0; + const n = +x; + if (x !== n + "") return undefined; + return n; + } + export function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined; export function tryCast(value: T, test: (value: T) => boolean): T | undefined; export function tryCast(value: T, test: (value: T) => boolean): T | undefined { diff --git a/src/compiler/debug.ts b/src/compiler/debug.ts index 9adc4095054..42a9b8c1e0d 100644 --- a/src/compiler/debug.ts +++ b/src/compiler/debug.ts @@ -10,7 +10,7 @@ namespace ts { return currentAssertionLevel >= level; } - export function assert(expression: boolean, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): void { + export function assert(expression: boolean, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): asserts expression { if (!expression) { if (verboseDebugInfo) { message += "\r\nVerbose Debug Information: " + (typeof verboseDebugInfo === "string" ? verboseDebugInfo : verboseDebugInfo()); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 2dc894e71fb..b1ee373fa61 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2879,6 +2879,10 @@ "category": "Message", "code": 2782 }, + "Type '{0}' cannot be used as an index type in an offset or range type.": { + "category": "Error", + "code": 2783 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index a5eb13647e9..c5a839a307f 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1332,6 +1332,8 @@ namespace ts { return emitConditionalType(node); case SyntaxKind.InferType: return emitInferType(node); + case SyntaxKind.InverseOffsetType: + return emitOffsetType(node); case SyntaxKind.ParenthesizedType: return emitParenthesizedType(node); case SyntaxKind.ExpressionWithTypeArguments: @@ -1344,6 +1346,8 @@ namespace ts { return emitIndexedAccessType(node); case SyntaxKind.MappedType: return emitMappedType(node); + case SyntaxKind.RangeType: + return emitRangeType(node); case SyntaxKind.LiteralType: return emitLiteralType(node); case SyntaxKind.ImportType: @@ -2131,6 +2135,11 @@ namespace ts { emit(node.typeParameter); } + function emitOffsetType(node: InverseOffsetTypeNode) { + writePunctuation("^"); + emit(node.indexType); + } + function emitParenthesizedType(node: ParenthesizedTypeNode) { writePunctuation("("); emit(node.type); @@ -2196,6 +2205,15 @@ namespace ts { writePunctuation("}"); } + function emitRangeType(node: RangeTypeNode) { + emit(node.objectType); + writePunctuation("["); + emit(node.startType); + writePunctuation(":"); + emit(node.endType); + writePunctuation("]"); + } + function emitLiteralType(node: LiteralTypeNode) { emitExpression(node.literal); } diff --git a/src/compiler/factoryPublic.ts b/src/compiler/factoryPublic.ts index 2c59abbb494..2f0cf58dfd7 100644 --- a/src/compiler/factoryPublic.ts +++ b/src/compiler/factoryPublic.ts @@ -904,6 +904,18 @@ namespace ts { : node; } + export function createInverseOffsetTypeNode(indexType: TypeNode) { + const node = createSynthesizedNode(SyntaxKind.InverseOffsetType); + node.indexType = parenthesizeElementTypeMember(indexType); + return node; + } + + export function updateInverseOffsetTypeNode(node: InverseOffsetTypeNode, indexType: TypeNode) { + return node.indexType !== indexType + ? updateNode(createInverseOffsetTypeNode(indexType), node) + : node; + } + export function createImportTypeNode(argument: TypeNode, qualifier?: EntityName, typeArguments?: readonly TypeNode[], isTypeOf?: boolean) { const node = createSynthesizedNode(SyntaxKind.ImportType); node.argument = argument; @@ -958,7 +970,7 @@ namespace ts { return node; } - export function updateIndexedAccessTypeNode(node: IndexedAccessTypeNode, objectType: TypeNode, indexType: TypeNode) { + export function updateIndexedAccessTypeNode(node: IndexedAccessTypeNode, objectType: TypeNode, indexType: TypeNode): IndexedAccessTypeNode { return node.objectType !== objectType || node.indexType !== indexType ? updateNode(createIndexedAccessTypeNode(objectType, indexType), node) @@ -983,6 +995,22 @@ namespace ts { : node; } + export function createRangeTypeNode(objectType: TypeNode, startType: TypeNode | undefined, endType: TypeNode | undefined) { + const node = createSynthesizedNode(SyntaxKind.RangeType) as RangeTypeNode; + node.objectType = objectType; + node.startType = startType; + node.endType = endType; + return node; + } + + export function updateRangeTypeNode(node: RangeTypeNode, objectType: TypeNode, startType: TypeNode | undefined, endType: TypeNode | undefined) { + return node.objectType !== objectType + || node.startType !== startType + || node.endType !== endType + ? updateNode(createRangeTypeNode(objectType, startType, endType), node) + : node; + } + export function createLiteralTypeNode(literal: LiteralTypeNode["literal"]) { const node = createSynthesizedNode(SyntaxKind.LiteralType) as LiteralTypeNode; node.literal = literal; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index e990c0f675c..90cc585f840 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -190,6 +190,8 @@ namespace ts { visitNode(cbNode, (node).falseType); case SyntaxKind.InferType: return visitNode(cbNode, (node).typeParameter); + case SyntaxKind.InverseOffsetType: + return visitNode(cbNode, (node).indexType); case SyntaxKind.ImportType: return visitNode(cbNode, (node).argument) || visitNode(cbNode, (node).qualifier) || @@ -205,6 +207,10 @@ namespace ts { visitNode(cbNode, (node).typeParameter) || visitNode(cbNode, (node).questionToken) || visitNode(cbNode, (node).type); + case SyntaxKind.RangeType: + return visitNode(cbNode, (node).objectType) || + visitNode(cbNode, (node).startType) || + visitNode(cbNode, (node).endType); case SyntaxKind.LiteralType: return visitNode(cbNode, (node).literal); case SyntaxKind.ObjectBindingPattern: @@ -3165,6 +3171,7 @@ namespace ts { case SyntaxKind.InferKeyword: case SyntaxKind.ImportKeyword: case SyntaxKind.AssertsKeyword: + case SyntaxKind.CaretToken: return true; case SyntaxKind.FunctionKeyword: return !inStartOfParameter; @@ -3200,10 +3207,19 @@ namespace ts { break; case SyntaxKind.OpenBracketToken: parseExpected(SyntaxKind.OpenBracketToken); - if (isStartOfType()) { + const indexType = isStartOfType() ? parseType() : undefined; + if (parseOptionalToken(SyntaxKind.ColonToken)) { + const node = createNode(SyntaxKind.RangeType, type.pos) as RangeTypeNode; + node.objectType = type; + node.startType = indexType; + node.endType = isStartOfType() ? parseType() : undefined; + parseExpected(SyntaxKind.CloseBracketToken); + type = finishNode(node); + } + else if (indexType) { const node = createNode(SyntaxKind.IndexedAccessType, type.pos) as IndexedAccessTypeNode; node.objectType = type; - node.indexType = parseType(); + node.indexType = indexType; parseExpected(SyntaxKind.CloseBracketToken); type = finishNode(node); } @@ -3245,6 +3261,13 @@ namespace ts { return finishNode(node); } + function parseOffsetType(): InverseOffsetTypeNode { + const node = createNode(SyntaxKind.InverseOffsetType); + parseExpected(SyntaxKind.CaretToken); + node.indexType = parseType(); + return finishNode(node); + } + function parseTypeOperatorOrHigher(): TypeNode { const operator = token(); switch (operator) { @@ -3254,6 +3277,8 @@ namespace ts { return parseTypeOperator(operator); case SyntaxKind.InferKeyword: return parseInferType(); + case SyntaxKind.CaretToken: + return parseOffsetType(); } return parsePostfixTypeOrHigher(); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 9b913c2f17d..4867423f9cb 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -322,11 +322,13 @@ namespace ts { IntersectionType, ConditionalType, InferType, + InverseOffsetType, ParenthesizedType, ThisType, TypeOperator, IndexedAccessType, MappedType, + RangeType, LiteralType, ImportType, // Binding patterns @@ -1310,6 +1312,11 @@ namespace ts { typeParameter: TypeParameterDeclaration; } + export interface InverseOffsetTypeNode extends TypeNode { + kind: SyntaxKind.InverseOffsetType; + indexType: TypeNode; + } + export interface ParenthesizedTypeNode extends TypeNode { kind: SyntaxKind.ParenthesizedType; type: TypeNode; @@ -1340,6 +1347,13 @@ namespace ts { type?: TypeNode; } + export interface RangeTypeNode extends TypeNode { + kind: SyntaxKind.RangeType; + objectType: TypeNode; + startType: TypeNode | undefined; + endType: TypeNode | undefined; + } + export interface LiteralTypeNode extends TypeNode { kind: SyntaxKind.LiteralType; literal: BooleanLiteral | LiteralExpression | PrefixUnaryExpression; @@ -4153,6 +4167,7 @@ namespace ts { Function = "__function", // Unnamed function expression Computed = "__computed", // Computed property name declaration with dynamic name Resolving = "__resolving__", // Indicator symbol used to mark partially resolved type aliases + Boundary = "__boundary", // Indicator symbol for the unspecified upper or lower bound in a range type (i.e., `T[:]`) ExportEquals = "export=", // Export assignment symbol Default = "default", // Default export symbol (technically not wholly internal, but included here for usability) This = "this", @@ -4281,6 +4296,8 @@ namespace ts { Conditional = 1 << 24, // T extends U ? X : Y Substitution = 1 << 25, // Type parameter substitution NonPrimitive = 1 << 26, // intrinsic object type + Range = 1 << 27, // T[X:Y] + InverseOffset = 1 << 28, // ^T /* @internal */ AnyOrUnknown = Any | Unknown, @@ -4299,7 +4316,7 @@ namespace ts { /* @internal */ Primitive = String | Number | BigInt | Boolean | Enum | EnumLiteral | ESSymbol | Void | Undefined | Null | Literal | UniqueESSymbol, StringLike = String | StringLiteral, - NumberLike = Number | NumberLiteral | Enum, + NumberLike = Number | NumberLiteral | Enum | InverseOffset, BigIntLike = BigInt | BigIntLiteral, BooleanLike = Boolean | BooleanLiteral, EnumLike = Enum | EnumLiteral, @@ -4309,15 +4326,15 @@ namespace ts { DisjointDomains = NonPrimitive | StringLike | NumberLike | BigIntLike | BooleanLike | ESSymbolLike | VoidLike | Null, UnionOrIntersection = Union | Intersection, StructuredType = Object | Union | Intersection, - TypeVariable = TypeParameter | IndexedAccess, - InstantiableNonPrimitive = TypeVariable | Conditional | Substitution, - InstantiablePrimitive = Index, + TypeVariable = TypeParameter | IndexedAccess | Range, + InstantiableNonPrimitive = TypeVariable | Conditional | Substitution | Range, + InstantiablePrimitive = Index | InverseOffset, Instantiable = InstantiableNonPrimitive | InstantiablePrimitive, StructuredOrInstantiable = StructuredType | Instantiable, /* @internal */ ObjectFlagsType = Any | Nullable | Never | Object | Union | Intersection, /* @internal */ - Simplifiable = IndexedAccess | Conditional, + Simplifiable = IndexedAccess | Range | Conditional, // 'Narrowable' types are types where narrowing actually narrows. // This *should* be every type other than null, undefined, void, and never Narrowable = Any | Unknown | StructuredOrInstantiable | StringLike | NumberLike | BigIntLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive, @@ -4687,7 +4704,7 @@ namespace ts { simplifiedForWriting?: Type; } - export type TypeVariable = TypeParameter | IndexedAccessType; + export type TypeVariable = TypeParameter | IndexedAccessType | RangeType; // keyof T types (TypeFlags.Index) export interface IndexType extends InstantiableType { @@ -4738,6 +4755,20 @@ namespace ts { substitute: Type; // Type to substitute for type parameter } + // ^T (TypeFlags.Offset) + export interface InverseOffsetType extends InstantiableType { + indexType: Type; + } + + // T[X:Y] (TypeFlags.Range) + export interface RangeType extends InstantiableType { + objectType: Type; + startType: Type; + endType: Type; + simplifiedForReading?: Type; + simplifiedForWriting?: Type; + } + /* @internal */ export const enum JsxReferenceKind { Component, diff --git a/src/compiler/utilitiesPublic.ts b/src/compiler/utilitiesPublic.ts index cc36af32f2c..59b1b477d41 100644 --- a/src/compiler/utilitiesPublic.ts +++ b/src/compiler/utilitiesPublic.ts @@ -1000,6 +1000,10 @@ namespace ts { return node.kind === SyntaxKind.InferType; } + export function isOffsetTypeNode(node: Node): node is InverseOffsetTypeNode { + return node.kind === SyntaxKind.InverseOffsetType; + } + export function isParenthesizedTypeNode(node: Node): node is ParenthesizedTypeNode { return node.kind === SyntaxKind.ParenthesizedType; } diff --git a/src/compiler/visitorPublic.ts b/src/compiler/visitorPublic.ts index 3cbd60ff516..50eb938bd34 100644 --- a/src/compiler/visitorPublic.ts +++ b/src/compiler/visitorPublic.ts @@ -398,6 +398,10 @@ namespace ts { return updateInferTypeNode(node, visitNode((node).typeParameter, visitor, isTypeParameterDeclaration)); + case SyntaxKind.InverseOffsetType: + return updateInverseOffsetTypeNode(node, + visitNode((node).indexType, visitor, isTypeNode)); + case SyntaxKind.ImportType: return updateImportTypeNode(node, visitNode((node).argument, visitor, isTypeNode), @@ -426,6 +430,12 @@ namespace ts { visitNode((node).questionToken, tokenVisitor, isToken), visitNode((node).type, visitor, isTypeNode)); + case SyntaxKind.RangeType: + return updateRangeTypeNode((node), + visitNode((node).objectType, visitor, isTypeNode), + visitNode((node).startType, visitor, isTypeNode), + visitNode((node).endType, visitor, isTypeNode)); + case SyntaxKind.LiteralType: return updateLiteralTypeNode(node, visitNode((node).literal, visitor, isExpression)); diff --git a/src/services/codefixes/fixSpelling.ts b/src/services/codefixes/fixSpelling.ts index 637f7751e6b..a2c9683f95d 100644 --- a/src/services/codefixes/fixSpelling.ts +++ b/src/services/codefixes/fixSpelling.ts @@ -35,16 +35,17 @@ namespace ts.codefix { const checker = context.program.getTypeChecker(); let suggestedSymbol: Symbol | undefined; - if (isPropertyAccessExpression(node.parent) && node.parent.name === node) { + const { parent } = node; + if (isPropertyAccessExpression(parent) && parent.name === node) { Debug.assert(isIdentifierOrPrivateIdentifier(node), "Expected an identifier for spelling (property access)"); - let containingType = checker.getTypeAtLocation(node.parent.expression); + let containingType = checker.getTypeAtLocation(parent.expression); if (node.parent.flags & NodeFlags.OptionalChain) { containingType = checker.getNonNullableType(containingType); } const name = node as Identifier | PrivateIdentifier; suggestedSymbol = checker.getSuggestedSymbolForNonexistentProperty(name, containingType); } - else if (isImportSpecifier(node.parent) && node.parent.name === node) { + else if (isImportSpecifier(parent) && parent.name === node) { Debug.assert(node.kind === SyntaxKind.Identifier, "Expected an identifier for spelling (import)"); const importDeclaration = findAncestor(node, isImportDeclaration)!; const resolvedSourceFile = getResolvedSourceFileFromImportDeclaration(sourceFile, context, importDeclaration); diff --git a/tests/baselines/reference/ranges.errors.txt b/tests/baselines/reference/ranges.errors.txt new file mode 100644 index 00000000000..e51ae09f1c1 --- /dev/null +++ b/tests/baselines/reference/ranges.errors.txt @@ -0,0 +1,196 @@ +tests/cases/conformance/types/range/semantics.ts(14,15): error TS2461: Type '{}' is not an array type. +tests/cases/conformance/types/range/semantics.ts(21,25): error TS2536: Type 'true' cannot be used to index type '[1, 2, 3]'. + + +==== tests/cases/conformance/types/range/semantics.ts (2 errors) ==== + // Rules (from https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c) + + // Semantic Rules + { + // - The result of a *Range Type* has the same mutability as its *object type*. + type T1 = [1, 2, 3][0:2]; + type T2 = (readonly [1, 2, 3])[0:2]; + } + + { + // - The *object type* of a *Range Type* is constrained to be an *Array Type* or a *Tuple Type*. + type T1 = [1, 2, 3][0:2]; + type T2 = number[][0:2]; + type T3 = {}[0:2]; // error + ~~~~~~~ +!!! error TS2461: Type '{}' is not an array type. + } + + { + // - The *start type* and *end type* of a *Range Type* are constrained to `string | number`. + type T1 = [1, 2, 3][0:2]; + type T2 = [1, 2, 3]["0":"2"]; + type T3 = [1, 2, 3][true:false]; // error + ~~~~ +!!! error TS2536: Type 'true' cannot be used to index type '[1, 2, 3]'. + } + + { + // - The *start type* of a *Range Type* is optional. If not present, the *lower bound type* (`0`) is used. + type T2 = [1, 2, 3][:1]; + + // - The *end type* of a *Range Type* is optional. If not present, the *upper bound type* (`^0`) is used. + type T3 = [1, 2, 3][1:]; + type T1 = [1, 2, 3][:]; + } + + { + // - If the *start type* or *end type* of a *Range Type* are negative-valued *Numeric Literal Types* (or numeric index-valued *String Literal Types*), they are treated + // as an *Inverse Offset Type* for the absolute value of the numeric index value of the respective type. + // - NOTE: This does not work for `-0` as JavaScript generally treats `-0` and `0` as the same value except for a few small corner cases and we must + // align with this behavior. + type T1 = [1, 2, 3, 4][1:-1]; + type T2 = [1, 2, 3, 4][1:-0]; + } + + { + // - If the *object type* is a "generic object type", or if either of the *start type* or *end type* are + // "generic index types", then the operation is deferred until it they can be instantiated. + type T1 = A[0:2]; + type T2 = T1<[1, 2, 3]>; + type T3 = T1; + + type T4 = [1, 2, 3][X:]; + type T5 = T4<1>; + + type T6 = [1, 2, 3][:Y]; + type T7 = T6<^1>; + } + + { + // - If either the *object type*, *start type*, or *end type* are *Union Types*, the result is distributed over + // each constituent in the following manner: + // - The *object type* is distributed over any *Inverse Offset Type* constituent of *start type* or *end type*. + // This is necessary as an *Inverse Offset Type* can have a different outcome depending on the *object type* + // it is resolved against. + // - The *start type* and *end type* are distributed over each constituent of the *object type*. + // - The *object type* is distributed over each constituent of the *start type* and *end type*. + // - The results of the distribution are either an *Intersection Type* (if the *Range Type* was a "write" + // location), or a *Union Type* (if the *Range Type* was a "read" location). + type T1 = ([1, 2, 3] | [2, 3, 4])[0:2]; + type T2 = [1, 2, 3][0|1:2]; + type T3 = [1, 2, 3][0:1|2]; + type T4 = ([1, 2, 3] | [2, 3, 4])[0|1:2|3]; + type T5 = ([1, 2, 3] | [9, 8])[0:^1]; + } + + { + // - Otherwise (or for each constituent of the distribution), + { + // - If neither the *start type* nor the *end type* are *String Literal Types* or a *Numeric Literal Types*, then + // - Return an *Array Type* for the union of each element of the *object type*: `T[any:any] -> T[number][]`. + type T1 = [1, 2, 3][number:number]; + type T2 = [1, 2, 3][string:string]; + type T3 = [1, 2, 3][any:any]; + type T4 = [1, 2, 3][never:never]; + } + { + // - If the *start type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *end type*, but with a *start type* of `0`: `T[any:Y] -> T[:Y][number][]`. + type T1 = [1, 2, 3][number:2]; + type T2 = [1, 2, 3][string:2]; + type T3 = [1, 2, 3][any:2]; + type T4 = [1, 2, 3][never:2]; + } + { + // - If the *end type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *start type*, but with an *end type* of `^0`: `T[X:any] -> T[X:][number][]`. + type T1 = [1, 2, 3][1:number]; + type T2 = [1, 2, 3][1:string]; + type T3 = [1, 2, 3][1:any]; + type T4 = [1, 2, 3][1:never]; + } + { + // - If the *start type* is the *lower bound type* (`0`) and the *end type* is the *upper bound type* (`^0`), + // then we are including all elements: return the *object type* of the *Range Type*. + type T1 = [1, 2, 3][0:^0]; + } + { + // - If the *start type* is the *upper bound type* (`^0`) or the *end type* is the *lower bound type* (`0`), + // then we are including no elements: return the empty *Tuple Type* (`[]`). + type T1 = [1, 2, 3][^0:^0]; + type T2 = [1, 2, 3][0:0]; + } + { + // - If the *object type* is an *Array Type*, then + { + // - If the signs of both the *start type* and *end type* agree, then return + // a fixed-length *Tuple Type* with a minimum length of `0` for the difference between the *end type* and + // the *start type* whose elements are the element type of the *object type*: `T[][0:1] -> [T?]`. + type T1 = number[][0:2]; + type T2 = number[][^2:^0]; + } + { + // - If the *start type* is an *Inverse Offset Type* and the *end type* is not, we can create a tuple of + // `min(abs(start), end)` optional elements. + type T1 = number[][^2:4]; + } + { + // - Otherwise, we cannot derive a fixed length: return the *object type* of the *Range Type*. + type T1 = number[][0:^2]; + } + } + { + // - Otherwise, the *object type* is a *Tuple Type*: + { + // - If the *object type* has a rest element, then + { + // - If the *start type* is an *Inverse Offset Type*, return an *Array Type* for the union of each + // *optional type* and the *rest element type*, along with the set of `n` right-most required + // elements of the *object type* where `n` is the absolute value of the *start type*. + type T1 = [1, 2, 3, ...4[]][^1:]; + type T2 = [1, 2, 3, ...4[]][^2:]; + type T3 = [1, 2, 3, ...4[]][^3:]; + type T4 = [1, 2, 3?, ...4[]][^1:]; + type T5 = [1, 2, 3?, ...4[]][^2:]; + type T6 = [1, 2, 3?, ...4[]][^3:]; + } + { + // - If the *end type* is an *Inverse Offset Type*, return a *Tuple Type* of the elements of the + // *object type* starting from the index at *start type*, but whose minimum length is reduced by the + // absolute value of the *end type*. + type T1 = [1, 2, 3, ...4[]][1:^1]; + type T2 = [1, 2, 3, ...4[]][1:^2]; + type T3 = [1, 2, 3, ...4[]][1:^3]; + type T4 = [1, 2, 3?, ...4[]][1:^1]; + type T5 = [1, 2, 3?, ...4[]][1:^2]; + type T6 = [1, 2, 3?, ...4[]][1:^3]; + } + } + { + // - Otherwise, + { + // - Clamp the *start type* and *end type* to values between `0` and the length of *object type*. + type T1 = [1, 2, 3][^5:10]; + } + { + // - Return a *Tuple Type* for the elements of *object type* starting at *start type* and ending at + // *end type*. + type T1 = [1, 2, 3][1: 2]; + } + } + } + } + +==== tests/cases/conformance/types/range/assignability.ts (0 errors) ==== + // - `S` is assignable to `T[X:Y]` if `S` is assignable to `C`, where `C` is the base constraint of `T[X:Y]` for writing. + function f1(t: T[U:^1], s: [3] & [2, 3]) { + t = s; + } + + // - `S[X:Y]` is assignable to `T[]` if `S[number]` is assignable to `T`. + function f2(t: T[], s: S[0:2]) { + t = s; + } + + // - `S[XS:YS]` is assignable to `T[XT:YT]` if `S` is assignable to `T`, `XS` is assignable to `XT`, and `YS` is assignable to `YT`. + function f3(t: T[1 | 2:3], s: S[1:3]) { + t = s; + } \ No newline at end of file diff --git a/tests/baselines/reference/ranges.js b/tests/baselines/reference/ranges.js new file mode 100644 index 00000000000..ca53a4b3008 --- /dev/null +++ b/tests/baselines/reference/ranges.js @@ -0,0 +1,262 @@ +//// [tests/cases/conformance/types/range/ranges.ts] //// + +//// [semantics.ts] +// Rules (from https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c) + +// Semantic Rules +{ + // - The result of a *Range Type* has the same mutability as its *object type*. + type T1 = [1, 2, 3][0:2]; + type T2 = (readonly [1, 2, 3])[0:2]; +} + +{ + // - The *object type* of a *Range Type* is constrained to be an *Array Type* or a *Tuple Type*. + type T1 = [1, 2, 3][0:2]; + type T2 = number[][0:2]; + type T3 = {}[0:2]; // error +} + +{ + // - The *start type* and *end type* of a *Range Type* are constrained to `string | number`. + type T1 = [1, 2, 3][0:2]; + type T2 = [1, 2, 3]["0":"2"]; + type T3 = [1, 2, 3][true:false]; // error +} + +{ + // - The *start type* of a *Range Type* is optional. If not present, the *lower bound type* (`0`) is used. + type T2 = [1, 2, 3][:1]; + + // - The *end type* of a *Range Type* is optional. If not present, the *upper bound type* (`^0`) is used. + type T3 = [1, 2, 3][1:]; + type T1 = [1, 2, 3][:]; +} + +{ + // - If the *start type* or *end type* of a *Range Type* are negative-valued *Numeric Literal Types* (or numeric index-valued *String Literal Types*), they are treated + // as an *Inverse Offset Type* for the absolute value of the numeric index value of the respective type. + // - NOTE: This does not work for `-0` as JavaScript generally treats `-0` and `0` as the same value except for a few small corner cases and we must + // align with this behavior. + type T1 = [1, 2, 3, 4][1:-1]; + type T2 = [1, 2, 3, 4][1:-0]; +} + +{ + // - If the *object type* is a "generic object type", or if either of the *start type* or *end type* are + // "generic index types", then the operation is deferred until it they can be instantiated. + type T1 = A[0:2]; + type T2 = T1<[1, 2, 3]>; + type T3 = T1; + + type T4 = [1, 2, 3][X:]; + type T5 = T4<1>; + + type T6 = [1, 2, 3][:Y]; + type T7 = T6<^1>; +} + +{ + // - If either the *object type*, *start type*, or *end type* are *Union Types*, the result is distributed over + // each constituent in the following manner: + // - The *object type* is distributed over any *Inverse Offset Type* constituent of *start type* or *end type*. + // This is necessary as an *Inverse Offset Type* can have a different outcome depending on the *object type* + // it is resolved against. + // - The *start type* and *end type* are distributed over each constituent of the *object type*. + // - The *object type* is distributed over each constituent of the *start type* and *end type*. + // - The results of the distribution are either an *Intersection Type* (if the *Range Type* was a "write" + // location), or a *Union Type* (if the *Range Type* was a "read" location). + type T1 = ([1, 2, 3] | [2, 3, 4])[0:2]; + type T2 = [1, 2, 3][0|1:2]; + type T3 = [1, 2, 3][0:1|2]; + type T4 = ([1, 2, 3] | [2, 3, 4])[0|1:2|3]; + type T5 = ([1, 2, 3] | [9, 8])[0:^1]; +} + +{ + // - Otherwise (or for each constituent of the distribution), + { + // - If neither the *start type* nor the *end type* are *String Literal Types* or a *Numeric Literal Types*, then + // - Return an *Array Type* for the union of each element of the *object type*: `T[any:any] -> T[number][]`. + type T1 = [1, 2, 3][number:number]; + type T2 = [1, 2, 3][string:string]; + type T3 = [1, 2, 3][any:any]; + type T4 = [1, 2, 3][never:never]; + } + { + // - If the *start type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *end type*, but with a *start type* of `0`: `T[any:Y] -> T[:Y][number][]`. + type T1 = [1, 2, 3][number:2]; + type T2 = [1, 2, 3][string:2]; + type T3 = [1, 2, 3][any:2]; + type T4 = [1, 2, 3][never:2]; + } + { + // - If the *end type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *start type*, but with an *end type* of `^0`: `T[X:any] -> T[X:][number][]`. + type T1 = [1, 2, 3][1:number]; + type T2 = [1, 2, 3][1:string]; + type T3 = [1, 2, 3][1:any]; + type T4 = [1, 2, 3][1:never]; + } + { + // - If the *start type* is the *lower bound type* (`0`) and the *end type* is the *upper bound type* (`^0`), + // then we are including all elements: return the *object type* of the *Range Type*. + type T1 = [1, 2, 3][0:^0]; + } + { + // - If the *start type* is the *upper bound type* (`^0`) or the *end type* is the *lower bound type* (`0`), + // then we are including no elements: return the empty *Tuple Type* (`[]`). + type T1 = [1, 2, 3][^0:^0]; + type T2 = [1, 2, 3][0:0]; + } + { + // - If the *object type* is an *Array Type*, then + { + // - If the signs of both the *start type* and *end type* agree, then return + // a fixed-length *Tuple Type* with a minimum length of `0` for the difference between the *end type* and + // the *start type* whose elements are the element type of the *object type*: `T[][0:1] -> [T?]`. + type T1 = number[][0:2]; + type T2 = number[][^2:^0]; + } + { + // - If the *start type* is an *Inverse Offset Type* and the *end type* is not, we can create a tuple of + // `min(abs(start), end)` optional elements. + type T1 = number[][^2:4]; + } + { + // - Otherwise, we cannot derive a fixed length: return the *object type* of the *Range Type*. + type T1 = number[][0:^2]; + } + } + { + // - Otherwise, the *object type* is a *Tuple Type*: + { + // - If the *object type* has a rest element, then + { + // - If the *start type* is an *Inverse Offset Type*, return an *Array Type* for the union of each + // *optional type* and the *rest element type*, along with the set of `n` right-most required + // elements of the *object type* where `n` is the absolute value of the *start type*. + type T1 = [1, 2, 3, ...4[]][^1:]; + type T2 = [1, 2, 3, ...4[]][^2:]; + type T3 = [1, 2, 3, ...4[]][^3:]; + type T4 = [1, 2, 3?, ...4[]][^1:]; + type T5 = [1, 2, 3?, ...4[]][^2:]; + type T6 = [1, 2, 3?, ...4[]][^3:]; + } + { + // - If the *end type* is an *Inverse Offset Type*, return a *Tuple Type* of the elements of the + // *object type* starting from the index at *start type*, but whose minimum length is reduced by the + // absolute value of the *end type*. + type T1 = [1, 2, 3, ...4[]][1:^1]; + type T2 = [1, 2, 3, ...4[]][1:^2]; + type T3 = [1, 2, 3, ...4[]][1:^3]; + type T4 = [1, 2, 3?, ...4[]][1:^1]; + type T5 = [1, 2, 3?, ...4[]][1:^2]; + type T6 = [1, 2, 3?, ...4[]][1:^3]; + } + } + { + // - Otherwise, + { + // - Clamp the *start type* and *end type* to values between `0` and the length of *object type*. + type T1 = [1, 2, 3][^5:10]; + } + { + // - Return a *Tuple Type* for the elements of *object type* starting at *start type* and ending at + // *end type*. + type T1 = [1, 2, 3][1: 2]; + } + } + } +} + +//// [assignability.ts] +// - `S` is assignable to `T[X:Y]` if `S` is assignable to `C`, where `C` is the base constraint of `T[X:Y]` for writing. +function f1(t: T[U:^1], s: [3] & [2, 3]) { + t = s; +} + +// - `S[X:Y]` is assignable to `T[]` if `S[number]` is assignable to `T`. +function f2(t: T[], s: S[0:2]) { + t = s; +} + +// - `S[XS:YS]` is assignable to `T[XT:YT]` if `S` is assignable to `T`, `XS` is assignable to `XT`, and `YS` is assignable to `YT`. +function f3(t: T[1 | 2:3], s: S[1:3]) { + t = s; +} + +//// [semantics.js] +"use strict"; +// Rules (from https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c) +// Semantic Rules +{ +} +{ +} +{ +} +{ +} +{ +} +{ +} +{ +} +{ + // - Otherwise (or for each constituent of the distribution), + { + } + { + } + { + } + { + } + { + } + { + // - If the *object type* is an *Array Type*, then + { + } + { + } + { + } + } + { + // - Otherwise, the *object type* is a *Tuple Type*: + { + // - If the *object type* has a rest element, then + { + } + { + } + } + { + // - Otherwise, + { + } + { + } + } + } +} +//// [assignability.js] +"use strict"; +// - `S` is assignable to `T[X:Y]` if `S` is assignable to `C`, where `C` is the base constraint of `T[X:Y]` for writing. +function f1(t, s) { + t = s; +} +// - `S[X:Y]` is assignable to `T[]` if `S[number]` is assignable to `T`. +function f2(t, s) { + t = s; +} +// - `S[XS:YS]` is assignable to `T[XT:YT]` if `S` is assignable to `T`, `XS` is assignable to `XT`, and `YS` is assignable to `YT`. +function f3(t, s) { + t = s; +} diff --git a/tests/baselines/reference/ranges.symbols b/tests/baselines/reference/ranges.symbols new file mode 100644 index 00000000000..2a8470c51ad --- /dev/null +++ b/tests/baselines/reference/ranges.symbols @@ -0,0 +1,326 @@ +=== tests/cases/conformance/types/range/semantics.ts === +// Rules (from https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c) + +// Semantic Rules +{ + // - The result of a *Range Type* has the same mutability as its *object type*. + type T1 = [1, 2, 3][0:2]; +>T1 : Symbol(T1, Decl(semantics.ts, 3, 1)) + + type T2 = (readonly [1, 2, 3])[0:2]; +>T2 : Symbol(T2, Decl(semantics.ts, 5, 29)) +} + +{ + // - The *object type* of a *Range Type* is constrained to be an *Array Type* or a *Tuple Type*. + type T1 = [1, 2, 3][0:2]; +>T1 : Symbol(T1, Decl(semantics.ts, 9, 1)) + + type T2 = number[][0:2]; +>T2 : Symbol(T2, Decl(semantics.ts, 11, 29)) + + type T3 = {}[0:2]; // error +>T3 : Symbol(T3, Decl(semantics.ts, 12, 28)) +} + +{ + // - The *start type* and *end type* of a *Range Type* are constrained to `string | number`. + type T1 = [1, 2, 3][0:2]; +>T1 : Symbol(T1, Decl(semantics.ts, 16, 1)) + + type T2 = [1, 2, 3]["0":"2"]; +>T2 : Symbol(T2, Decl(semantics.ts, 18, 29)) + + type T3 = [1, 2, 3][true:false]; // error +>T3 : Symbol(T3, Decl(semantics.ts, 19, 33)) +} + +{ + // - The *start type* of a *Range Type* is optional. If not present, the *lower bound type* (`0`) is used. + type T2 = [1, 2, 3][:1]; +>T2 : Symbol(T2, Decl(semantics.ts, 23, 1)) + + // - The *end type* of a *Range Type* is optional. If not present, the *upper bound type* (`^0`) is used. + type T3 = [1, 2, 3][1:]; +>T3 : Symbol(T3, Decl(semantics.ts, 25, 28)) + + type T1 = [1, 2, 3][:]; +>T1 : Symbol(T1, Decl(semantics.ts, 28, 28)) +} + +{ + // - If the *start type* or *end type* of a *Range Type* are negative-valued *Numeric Literal Types* (or numeric index-valued *String Literal Types*), they are treated + // as an *Inverse Offset Type* for the absolute value of the numeric index value of the respective type. + // - NOTE: This does not work for `-0` as JavaScript generally treats `-0` and `0` as the same value except for a few small corner cases and we must + // align with this behavior. + type T1 = [1, 2, 3, 4][1:-1]; +>T1 : Symbol(T1, Decl(semantics.ts, 32, 1)) + + type T2 = [1, 2, 3, 4][1:-0]; +>T2 : Symbol(T2, Decl(semantics.ts, 37, 33)) +} + +{ + // - If the *object type* is a "generic object type", or if either of the *start type* or *end type* are + // "generic index types", then the operation is deferred until it they can be instantiated. + type T1 = A[0:2]; +>T1 : Symbol(T1, Decl(semantics.ts, 41, 1)) +>A : Symbol(A, Decl(semantics.ts, 44, 12)) +>A : Symbol(A, Decl(semantics.ts, 44, 12)) + + type T2 = T1<[1, 2, 3]>; +>T2 : Symbol(T2, Decl(semantics.ts, 44, 38)) +>T1 : Symbol(T1, Decl(semantics.ts, 41, 1)) + + type T3 = T1; +>T3 : Symbol(T3, Decl(semantics.ts, 45, 28)) +>T1 : Symbol(T1, Decl(semantics.ts, 41, 1)) + + type T4 = [1, 2, 3][X:]; +>T4 : Symbol(T4, Decl(semantics.ts, 46, 27)) +>X : Symbol(X, Decl(semantics.ts, 48, 12)) +>X : Symbol(X, Decl(semantics.ts, 48, 12)) + + type T5 = T4<1>; +>T5 : Symbol(T5, Decl(semantics.ts, 48, 46)) +>T4 : Symbol(T4, Decl(semantics.ts, 46, 27)) + + type T6 = [1, 2, 3][:Y]; +>T6 : Symbol(T6, Decl(semantics.ts, 49, 20)) +>Y : Symbol(Y, Decl(semantics.ts, 51, 12)) +>Y : Symbol(Y, Decl(semantics.ts, 51, 12)) + + type T7 = T6<^1>; +>T7 : Symbol(T7, Decl(semantics.ts, 51, 46)) +>T6 : Symbol(T6, Decl(semantics.ts, 49, 20)) +} + +{ + // - If either the *object type*, *start type*, or *end type* are *Union Types*, the result is distributed over + // each constituent in the following manner: + // - The *object type* is distributed over any *Inverse Offset Type* constituent of *start type* or *end type*. + // This is necessary as an *Inverse Offset Type* can have a different outcome depending on the *object type* + // it is resolved against. + // - The *start type* and *end type* are distributed over each constituent of the *object type*. + // - The *object type* is distributed over each constituent of the *start type* and *end type*. + // - The results of the distribution are either an *Intersection Type* (if the *Range Type* was a "write" + // location), or a *Union Type* (if the *Range Type* was a "read" location). + type T1 = ([1, 2, 3] | [2, 3, 4])[0:2]; +>T1 : Symbol(T1, Decl(semantics.ts, 55, 1)) + + type T2 = [1, 2, 3][0|1:2]; +>T2 : Symbol(T2, Decl(semantics.ts, 65, 43)) + + type T3 = [1, 2, 3][0:1|2]; +>T3 : Symbol(T3, Decl(semantics.ts, 66, 31)) + + type T4 = ([1, 2, 3] | [2, 3, 4])[0|1:2|3]; +>T4 : Symbol(T4, Decl(semantics.ts, 67, 31)) + + type T5 = ([1, 2, 3] | [9, 8])[0:^1]; +>T5 : Symbol(T5, Decl(semantics.ts, 68, 47)) +} + +{ + // - Otherwise (or for each constituent of the distribution), + { + // - If neither the *start type* nor the *end type* are *String Literal Types* or a *Numeric Literal Types*, then + // - Return an *Array Type* for the union of each element of the *object type*: `T[any:any] -> T[number][]`. + type T1 = [1, 2, 3][number:number]; +>T1 : Symbol(T1, Decl(semantics.ts, 74, 5)) + + type T2 = [1, 2, 3][string:string]; +>T2 : Symbol(T2, Decl(semantics.ts, 77, 43)) + + type T3 = [1, 2, 3][any:any]; +>T3 : Symbol(T3, Decl(semantics.ts, 78, 43)) + + type T4 = [1, 2, 3][never:never]; +>T4 : Symbol(T4, Decl(semantics.ts, 79, 37)) + } + { + // - If the *start type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *end type*, but with a *start type* of `0`: `T[any:Y] -> T[:Y][number][]`. + type T1 = [1, 2, 3][number:2]; +>T1 : Symbol(T1, Decl(semantics.ts, 82, 5)) + + type T2 = [1, 2, 3][string:2]; +>T2 : Symbol(T2, Decl(semantics.ts, 86, 38)) + + type T3 = [1, 2, 3][any:2]; +>T3 : Symbol(T3, Decl(semantics.ts, 87, 38)) + + type T4 = [1, 2, 3][never:2]; +>T4 : Symbol(T4, Decl(semantics.ts, 88, 35)) + } + { + // - If the *end type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *start type*, but with an *end type* of `^0`: `T[X:any] -> T[X:][number][]`. + type T1 = [1, 2, 3][1:number]; +>T1 : Symbol(T1, Decl(semantics.ts, 91, 5)) + + type T2 = [1, 2, 3][1:string]; +>T2 : Symbol(T2, Decl(semantics.ts, 95, 38)) + + type T3 = [1, 2, 3][1:any]; +>T3 : Symbol(T3, Decl(semantics.ts, 96, 38)) + + type T4 = [1, 2, 3][1:never]; +>T4 : Symbol(T4, Decl(semantics.ts, 97, 35)) + } + { + // - If the *start type* is the *lower bound type* (`0`) and the *end type* is the *upper bound type* (`^0`), + // then we are including all elements: return the *object type* of the *Range Type*. + type T1 = [1, 2, 3][0:^0]; +>T1 : Symbol(T1, Decl(semantics.ts, 100, 5)) + } + { + // - If the *start type* is the *upper bound type* (`^0`) or the *end type* is the *lower bound type* (`0`), + // then we are including no elements: return the empty *Tuple Type* (`[]`). + type T1 = [1, 2, 3][^0:^0]; +>T1 : Symbol(T1, Decl(semantics.ts, 105, 5)) + + type T2 = [1, 2, 3][0:0]; +>T2 : Symbol(T2, Decl(semantics.ts, 108, 35)) + } + { + // - If the *object type* is an *Array Type*, then + { + // - If the signs of both the *start type* and *end type* agree, then return + // a fixed-length *Tuple Type* with a minimum length of `0` for the difference between the *end type* and + // the *start type* whose elements are the element type of the *object type*: `T[][0:1] -> [T?]`. + type T1 = number[][0:2]; +>T1 : Symbol(T1, Decl(semantics.ts, 113, 9)) + + type T2 = number[][^2:^0]; +>T2 : Symbol(T2, Decl(semantics.ts, 117, 36)) + } + { + // - If the *start type* is an *Inverse Offset Type* and the *end type* is not, we can create a tuple of + // `min(abs(start), end)` optional elements. + type T1 = number[][^2:4]; +>T1 : Symbol(T1, Decl(semantics.ts, 120, 9)) + } + { + // - Otherwise, we cannot derive a fixed length: return the *object type* of the *Range Type*. + type T1 = number[][0:^2]; +>T1 : Symbol(T1, Decl(semantics.ts, 125, 9)) + } + } + { + // - Otherwise, the *object type* is a *Tuple Type*: + { + // - If the *object type* has a rest element, then + { + // - If the *start type* is an *Inverse Offset Type*, return an *Array Type* for the union of each + // *optional type* and the *rest element type*, along with the set of `n` right-most required + // elements of the *object type* where `n` is the absolute value of the *start type*. + type T1 = [1, 2, 3, ...4[]][^1:]; +>T1 : Symbol(T1, Decl(semantics.ts, 134, 13)) + + type T2 = [1, 2, 3, ...4[]][^2:]; +>T2 : Symbol(T2, Decl(semantics.ts, 138, 49)) + + type T3 = [1, 2, 3, ...4[]][^3:]; +>T3 : Symbol(T3, Decl(semantics.ts, 139, 49)) + + type T4 = [1, 2, 3?, ...4[]][^1:]; +>T4 : Symbol(T4, Decl(semantics.ts, 140, 49)) + + type T5 = [1, 2, 3?, ...4[]][^2:]; +>T5 : Symbol(T5, Decl(semantics.ts, 141, 50)) + + type T6 = [1, 2, 3?, ...4[]][^3:]; +>T6 : Symbol(T6, Decl(semantics.ts, 142, 50)) + } + { + // - If the *end type* is an *Inverse Offset Type*, return a *Tuple Type* of the elements of the + // *object type* starting from the index at *start type*, but whose minimum length is reduced by the + // absolute value of the *end type*. + type T1 = [1, 2, 3, ...4[]][1:^1]; +>T1 : Symbol(T1, Decl(semantics.ts, 145, 13)) + + type T2 = [1, 2, 3, ...4[]][1:^2]; +>T2 : Symbol(T2, Decl(semantics.ts, 149, 50)) + + type T3 = [1, 2, 3, ...4[]][1:^3]; +>T3 : Symbol(T3, Decl(semantics.ts, 150, 50)) + + type T4 = [1, 2, 3?, ...4[]][1:^1]; +>T4 : Symbol(T4, Decl(semantics.ts, 151, 50)) + + type T5 = [1, 2, 3?, ...4[]][1:^2]; +>T5 : Symbol(T5, Decl(semantics.ts, 152, 51)) + + type T6 = [1, 2, 3?, ...4[]][1:^3]; +>T6 : Symbol(T6, Decl(semantics.ts, 153, 51)) + } + } + { + // - Otherwise, + { + // - Clamp the *start type* and *end type* to values between `0` and the length of *object type*. + type T1 = [1, 2, 3][^5:10]; +>T1 : Symbol(T1, Decl(semantics.ts, 159, 13)) + } + { + // - Return a *Tuple Type* for the elements of *object type* starting at *start type* and ending at + // *end type*. + type T1 = [1, 2, 3][1: 2]; +>T1 : Symbol(T1, Decl(semantics.ts, 163, 13)) + } + } + } +} + +=== tests/cases/conformance/types/range/assignability.ts === +// - `S` is assignable to `T[X:Y]` if `S` is assignable to `C`, where `C` is the base constraint of `T[X:Y]` for writing. +function f1(t: T[U:^1], s: [3] & [2, 3]) { +>f1 : Symbol(f1, Decl(assignability.ts, 0, 0)) +>T : Symbol(T, Decl(assignability.ts, 1, 12)) +>U : Symbol(U, Decl(assignability.ts, 1, 35)) +>t : Symbol(t, Decl(assignability.ts, 1, 53)) +>T : Symbol(T, Decl(assignability.ts, 1, 12)) +>U : Symbol(U, Decl(assignability.ts, 1, 35)) +>s : Symbol(s, Decl(assignability.ts, 1, 64)) + + t = s; +>t : Symbol(t, Decl(assignability.ts, 1, 53)) +>s : Symbol(s, Decl(assignability.ts, 1, 64)) +} + +// - `S[X:Y]` is assignable to `T[]` if `S[number]` is assignable to `T`. +function f2(t: T[], s: S[0:2]) { +>f2 : Symbol(f2, Decl(assignability.ts, 3, 1)) +>T : Symbol(T, Decl(assignability.ts, 6, 12)) +>S : Symbol(S, Decl(assignability.ts, 6, 14)) +>T : Symbol(T, Decl(assignability.ts, 6, 12)) +>T : Symbol(T, Decl(assignability.ts, 6, 12)) +>T : Symbol(T, Decl(assignability.ts, 6, 12)) +>t : Symbol(t, Decl(assignability.ts, 6, 36)) +>T : Symbol(T, Decl(assignability.ts, 6, 12)) +>s : Symbol(s, Decl(assignability.ts, 6, 43)) +>S : Symbol(S, Decl(assignability.ts, 6, 14)) + + t = s; +>t : Symbol(t, Decl(assignability.ts, 6, 36)) +>s : Symbol(s, Decl(assignability.ts, 6, 43)) +} + +// - `S[XS:YS]` is assignable to `T[XT:YT]` if `S` is assignable to `T`, `XS` is assignable to `XT`, and `YS` is assignable to `YT`. +function f3(t: T[1 | 2:3], s: S[1:3]) { +>f3 : Symbol(f3, Decl(assignability.ts, 8, 1)) +>T : Symbol(T, Decl(assignability.ts, 11, 12)) +>S : Symbol(S, Decl(assignability.ts, 11, 51)) +>T : Symbol(T, Decl(assignability.ts, 11, 12)) +>t : Symbol(t, Decl(assignability.ts, 11, 65)) +>T : Symbol(T, Decl(assignability.ts, 11, 12)) +>s : Symbol(s, Decl(assignability.ts, 11, 79)) +>S : Symbol(S, Decl(assignability.ts, 11, 51)) + + t = s; +>t : Symbol(t, Decl(assignability.ts, 11, 65)) +>s : Symbol(s, Decl(assignability.ts, 11, 79)) +} diff --git a/tests/baselines/reference/ranges.types b/tests/baselines/reference/ranges.types new file mode 100644 index 00000000000..e3ff326b5d9 --- /dev/null +++ b/tests/baselines/reference/ranges.types @@ -0,0 +1,309 @@ +=== tests/cases/conformance/types/range/semantics.ts === +// Rules (from https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c) + +// Semantic Rules +{ + // - The result of a *Range Type* has the same mutability as its *object type*. + type T1 = [1, 2, 3][0:2]; +>T1 : [1, 2] + + type T2 = (readonly [1, 2, 3])[0:2]; +>T2 : readonly [1, 2] +} + +{ + // - The *object type* of a *Range Type* is constrained to be an *Array Type* or a *Tuple Type*. + type T1 = [1, 2, 3][0:2]; +>T1 : [1, 2] + + type T2 = number[][0:2]; +>T2 : [number?, number?] + + type T3 = {}[0:2]; // error +>T3 : any +} + +{ + // - The *start type* and *end type* of a *Range Type* are constrained to `string | number`. + type T1 = [1, 2, 3][0:2]; +>T1 : [1, 2] + + type T2 = [1, 2, 3]["0":"2"]; +>T2 : [1, 2] + + type T3 = [1, 2, 3][true:false]; // error +>T3 : any +>true : true +>false : false +} + +{ + // - The *start type* of a *Range Type* is optional. If not present, the *lower bound type* (`0`) is used. + type T2 = [1, 2, 3][:1]; +>T2 : [1] + + // - The *end type* of a *Range Type* is optional. If not present, the *upper bound type* (`^0`) is used. + type T3 = [1, 2, 3][1:]; +>T3 : [2, 3] + + type T1 = [1, 2, 3][:]; +>T1 : [1, 2, 3] +} + +{ + // - If the *start type* or *end type* of a *Range Type* are negative-valued *Numeric Literal Types* (or numeric index-valued *String Literal Types*), they are treated + // as an *Inverse Offset Type* for the absolute value of the numeric index value of the respective type. + // - NOTE: This does not work for `-0` as JavaScript generally treats `-0` and `0` as the same value except for a few small corner cases and we must + // align with this behavior. + type T1 = [1, 2, 3, 4][1:-1]; +>T1 : [2, 3] +>-1 : -1 +>1 : 1 + + type T2 = [1, 2, 3, 4][1:-0]; +>T2 : [] +>-0 : 0 +>0 : 0 +} + +{ + // - If the *object type* is a "generic object type", or if either of the *start type* or *end type* are + // "generic index types", then the operation is deferred until it they can be instantiated. + type T1 = A[0:2]; +>T1 : A[0:2] + + type T2 = T1<[1, 2, 3]>; +>T2 : [1, 2] + + type T3 = T1; +>T3 : [number?, number?] + + type T4 = [1, 2, 3][X:]; +>T4 : [1, 2, 3][X:] + + type T5 = T4<1>; +>T5 : [2, 3] + + type T6 = [1, 2, 3][:Y]; +>T6 : [1, 2, 3][:Y] + + type T7 = T6<^1>; +>T7 : [1, 2] +} + +{ + // - If either the *object type*, *start type*, or *end type* are *Union Types*, the result is distributed over + // each constituent in the following manner: + // - The *object type* is distributed over any *Inverse Offset Type* constituent of *start type* or *end type*. + // This is necessary as an *Inverse Offset Type* can have a different outcome depending on the *object type* + // it is resolved against. + // - The *start type* and *end type* are distributed over each constituent of the *object type*. + // - The *object type* is distributed over each constituent of the *start type* and *end type*. + // - The results of the distribution are either an *Intersection Type* (if the *Range Type* was a "write" + // location), or a *Union Type* (if the *Range Type* was a "read" location). + type T1 = ([1, 2, 3] | [2, 3, 4])[0:2]; +>T1 : [1, 2] | [2, 3] + + type T2 = [1, 2, 3][0|1:2]; +>T2 : [1, 2] | [2] + + type T3 = [1, 2, 3][0:1|2]; +>T3 : [1, 2] | [1] + + type T4 = ([1, 2, 3] | [2, 3, 4])[0|1:2|3]; +>T4 : [1, 2, 3] | [1, 2] | [2, 3] | [2, 3, 4] | [2] | [3, 4] | [3] + + type T5 = ([1, 2, 3] | [9, 8])[0:^1]; +>T5 : [1, 2] | [9] +} + +{ + // - Otherwise (or for each constituent of the distribution), + { + // - If neither the *start type* nor the *end type* are *String Literal Types* or a *Numeric Literal Types*, then + // - Return an *Array Type* for the union of each element of the *object type*: `T[any:any] -> T[number][]`. + type T1 = [1, 2, 3][number:number]; +>T1 : (3 | 1 | 2)[] + + type T2 = [1, 2, 3][string:string]; +>T2 : (3 | 1 | 2)[] + + type T3 = [1, 2, 3][any:any]; +>T3 : (3 | 1 | 2)[] + + type T4 = [1, 2, 3][never:never]; +>T4 : (3 | 1 | 2)[] + } + { + // - If the *start type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *end type*, but with a *start type* of `0`: `T[any:Y] -> T[:Y][number][]`. + type T1 = [1, 2, 3][number:2]; +>T1 : (1 | 2)[] + + type T2 = [1, 2, 3][string:2]; +>T2 : (1 | 2)[] + + type T3 = [1, 2, 3][any:2]; +>T3 : (1 | 2)[] + + type T4 = [1, 2, 3][never:2]; +>T4 : (1 | 2)[] + } + { + // - If the *end type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *start type*, but with an *end type* of `^0`: `T[X:any] -> T[X:][number][]`. + type T1 = [1, 2, 3][1:number]; +>T1 : (3 | 2)[] + + type T2 = [1, 2, 3][1:string]; +>T2 : (3 | 2)[] + + type T3 = [1, 2, 3][1:any]; +>T3 : (3 | 2)[] + + type T4 = [1, 2, 3][1:never]; +>T4 : (3 | 2)[] + } + { + // - If the *start type* is the *lower bound type* (`0`) and the *end type* is the *upper bound type* (`^0`), + // then we are including all elements: return the *object type* of the *Range Type*. + type T1 = [1, 2, 3][0:^0]; +>T1 : [1, 2, 3] + } + { + // - If the *start type* is the *upper bound type* (`^0`) or the *end type* is the *lower bound type* (`0`), + // then we are including no elements: return the empty *Tuple Type* (`[]`). + type T1 = [1, 2, 3][^0:^0]; +>T1 : [] + + type T2 = [1, 2, 3][0:0]; +>T2 : [] + } + { + // - If the *object type* is an *Array Type*, then + { + // - If the signs of both the *start type* and *end type* agree, then return + // a fixed-length *Tuple Type* with a minimum length of `0` for the difference between the *end type* and + // the *start type* whose elements are the element type of the *object type*: `T[][0:1] -> [T?]`. + type T1 = number[][0:2]; +>T1 : [number?, number?] + + type T2 = number[][^2:^0]; +>T2 : [number?, number?] + } + { + // - If the *start type* is an *Inverse Offset Type* and the *end type* is not, we can create a tuple of + // `min(abs(start), end)` optional elements. + type T1 = number[][^2:4]; +>T1 : [number?, number?] + } + { + // - Otherwise, we cannot derive a fixed length: return the *object type* of the *Range Type*. + type T1 = number[][0:^2]; +>T1 : number[] + } + } + { + // - Otherwise, the *object type* is a *Tuple Type*: + { + // - If the *object type* has a rest element, then + { + // - If the *start type* is an *Inverse Offset Type*, return an *Array Type* for the union of each + // *optional type* and the *rest element type*, along with the set of `n` right-most required + // elements of the *object type* where `n` is the absolute value of the *start type*. + type T1 = [1, 2, 3, ...4[]][^1:]; +>T1 : 4[] + + type T2 = [1, 2, 3, ...4[]][^2:]; +>T2 : (3 | 4)[] + + type T3 = [1, 2, 3, ...4[]][^3:]; +>T3 : (3 | 2 | 4)[] + + type T4 = [1, 2, 3?, ...4[]][^1:]; +>T4 : (3 | 4 | undefined)[] + + type T5 = [1, 2, 3?, ...4[]][^2:]; +>T5 : (3 | 2 | 4 | undefined)[] + + type T6 = [1, 2, 3?, ...4[]][^3:]; +>T6 : (3 | 2 | 4 | undefined)[] + } + { + // - If the *end type* is an *Inverse Offset Type*, return a *Tuple Type* of the elements of the + // *object type* starting from the index at *start type*, but whose minimum length is reduced by the + // absolute value of the *end type*. + type T1 = [1, 2, 3, ...4[]][1:^1]; +>T1 : [2, 3?, ...4[]] + + type T2 = [1, 2, 3, ...4[]][1:^2]; +>T2 : [2?, 3?, ...4[]] + + type T3 = [1, 2, 3, ...4[]][1:^3]; +>T3 : [2?, 3?, ...4[]] + + type T4 = [1, 2, 3?, ...4[]][1:^1]; +>T4 : [2?, (3 | undefined)?, ...4[]] + + type T5 = [1, 2, 3?, ...4[]][1:^2]; +>T5 : [2?, (3 | undefined)?, ...4[]] + + type T6 = [1, 2, 3?, ...4[]][1:^3]; +>T6 : [2?, (3 | undefined)?, ...4[]] + } + } + { + // - Otherwise, + { + // - Clamp the *start type* and *end type* to values between `0` and the length of *object type*. + type T1 = [1, 2, 3][^5:10]; +>T1 : [1, 2, 3] + } + { + // - Return a *Tuple Type* for the elements of *object type* starting at *start type* and ending at + // *end type*. + type T1 = [1, 2, 3][1: 2]; +>T1 : [2] + } + } + } +} + +=== tests/cases/conformance/types/range/assignability.ts === +// - `S` is assignable to `T[X:Y]` if `S` is assignable to `C`, where `C` is the base constraint of `T[X:Y]` for writing. +function f1(t: T[U:^1], s: [3] & [2, 3]) { +>f1 : (t: T[U:^1], s: [3] & [2, 3]) => void +>t : T[U:^1] +>s : [3] & [2, 3] + + t = s; +>t = s : [3] & [2, 3] +>t : T[U:^1] +>s : [3] & [2, 3] +} + +// - `S[X:Y]` is assignable to `T[]` if `S[number]` is assignable to `T`. +function f2(t: T[], s: S[0:2]) { +>f2 : (t: T[], s: S[0:2]) => void +>t : T[] +>s : S[0:2] + + t = s; +>t = s : S[0:2] +>t : T[] +>s : S[0:2] +} + +// - `S[XS:YS]` is assignable to `T[XT:YT]` if `S` is assignable to `T`, `XS` is assignable to `XT`, and `YS` is assignable to `YT`. +function f3(t: T[1 | 2:3], s: S[1:3]) { +>f3 : (t: T[1 | 2:3], s: S[1:3]) => void +>t : T[1 | 2:3] +>s : S[1:3] + + t = s; +>t = s : S[1:3] +>t : T[1 | 2:3] +>s : S[1:3] +} diff --git a/tests/cases/conformance/types/inverseOffset/inverseOffsets.ts b/tests/cases/conformance/types/inverseOffset/inverseOffsets.ts new file mode 100644 index 00000000000..de24305d389 --- /dev/null +++ b/tests/cases/conformance/types/inverseOffset/inverseOffsets.ts @@ -0,0 +1,52 @@ +// @strict: true +// Rules (from https://gist.github.com/rbuckton/53e335ce3d63686e229bc4ae25017756) + +// @filename: parsing.ts +// Parsing Rules +type A = 0; +type B = 1; +type T0 = ^A; +type T1 = ^A | B; +type T2 = ^(A | B); +type T3 = keyof ^A; +type T4 = ^A[]; // error +type T5 = (^A)[]; + +// @filename: semantics.ts +// Semantic Rules +// - An *Inverse Offset Type*'s *index type* is constrained to `string | number`. +type T6 = ^0; +type T7 = ^"0"; +type T8 = ^true; // error + +// - The *Inverse Offset Type* of an *Inverse Offset Type* `I` is the *index type* of `I`: `^^I -> I` +type T9 = ^^0; // inverse rule (double negation = non-negated) + +// - An *Inverse Offset Type* for negative *index type* is instead a *Literal Type* for the absolute value of the *index type*: `^-1 -> 1` +type T10 = ^-1; // inverse rule (double negation = non-negated) + +// - If the *index type* of an *Inverse Offset Type* is a union, the *Inverse Offset Type* is distributed over the union: `^(A | B) -> ^A | ^B` +type T11 = ^(0 | 1); // distributes + +// - An *Inverse Offset Type* is deferred until applied as the *index type* of an *Indexed Access Type* or *Range Type*. +type T12 = ^0; + +// - An *Inverse Offset Type* is deferred if its *index type* is generic. +type T13 = ^A; +type T14 = T13<1>; + +type AR = [1, 2, 3]; +type X = AR[^1]; // 3 + +// @filename: assignability.ts +// Assignability Rules +// - `^S` is assignable to `string | number`. +type Constrained0 = never; +type Constrained1 = never; +type T15 = Constrained0<^0>; +type T16 = Constrained1<^0>; // error + +// - `^S` is assignable to `^T` if `S` is assignable to `T`. +function f(s: ^S, t: ^T) { + t = s; +} diff --git a/tests/cases/conformance/types/range/ranges.ts b/tests/cases/conformance/types/range/ranges.ts new file mode 100644 index 00000000000..1f73559451c --- /dev/null +++ b/tests/cases/conformance/types/range/ranges.ts @@ -0,0 +1,189 @@ +// @strict: true +// Rules (from https://gist.github.com/rbuckton/5fd81582fdf86a34b45bae82d842304c) + +// @filename: semantics.ts +// Semantic Rules +{ + // - The result of a *Range Type* has the same mutability as its *object type*. + type T1 = [1, 2, 3][0:2]; + type T2 = (readonly [1, 2, 3])[0:2]; +} + +{ + // - The *object type* of a *Range Type* is constrained to be an *Array Type* or a *Tuple Type*. + type T1 = [1, 2, 3][0:2]; + type T2 = number[][0:2]; + type T3 = {}[0:2]; // error +} + +{ + // - The *start type* and *end type* of a *Range Type* are constrained to `string | number`. + type T1 = [1, 2, 3][0:2]; + type T2 = [1, 2, 3]["0":"2"]; + type T3 = [1, 2, 3][true:false]; // error +} + +{ + // - The *start type* of a *Range Type* is optional. If not present, the *lower bound type* (`0`) is used. + type T2 = [1, 2, 3][:1]; + + // - The *end type* of a *Range Type* is optional. If not present, the *upper bound type* (`^0`) is used. + type T3 = [1, 2, 3][1:]; + type T1 = [1, 2, 3][:]; +} + +{ + // - If the *start type* or *end type* of a *Range Type* are negative-valued *Numeric Literal Types* (or numeric index-valued *String Literal Types*), they are treated + // as an *Inverse Offset Type* for the absolute value of the numeric index value of the respective type. + // - NOTE: This does not work for `-0` as JavaScript generally treats `-0` and `0` as the same value except for a few small corner cases and we must + // align with this behavior. + type T1 = [1, 2, 3, 4][1:-1]; + type T2 = [1, 2, 3, 4][1:-0]; +} + +{ + // - If the *object type* is a "generic object type", or if either of the *start type* or *end type* are + // "generic index types", then the operation is deferred until it they can be instantiated. + type T1 = A[0:2]; + type T2 = T1<[1, 2, 3]>; + type T3 = T1; + + type T4 = [1, 2, 3][X:]; + type T5 = T4<1>; + + type T6 = [1, 2, 3][:Y]; + type T7 = T6<^1>; +} + +{ + // - If either the *object type*, *start type*, or *end type* are *Union Types*, the result is distributed over + // each constituent in the following manner: + // - The *object type* is distributed over any *Inverse Offset Type* constituent of *start type* or *end type*. + // This is necessary as an *Inverse Offset Type* can have a different outcome depending on the *object type* + // it is resolved against. + // - The *start type* and *end type* are distributed over each constituent of the *object type*. + // - The *object type* is distributed over each constituent of the *start type* and *end type*. + // - The results of the distribution are either an *Intersection Type* (if the *Range Type* was a "write" + // location), or a *Union Type* (if the *Range Type* was a "read" location). + type T1 = ([1, 2, 3] | [2, 3, 4])[0:2]; + type T2 = [1, 2, 3][0|1:2]; + type T3 = [1, 2, 3][0:1|2]; + type T4 = ([1, 2, 3] | [2, 3, 4])[0|1:2|3]; + type T5 = ([1, 2, 3] | [9, 8])[0:^1]; +} + +{ + // - Otherwise (or for each constituent of the distribution), + { + // - If neither the *start type* nor the *end type* are *String Literal Types* or a *Numeric Literal Types*, then + // - Return an *Array Type* for the union of each element of the *object type*: `T[any:any] -> T[number][]`. + type T1 = [1, 2, 3][number:number]; + type T2 = [1, 2, 3][string:string]; + type T3 = [1, 2, 3][any:any]; + type T4 = [1, 2, 3][never:never]; + } + { + // - If the *start type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *end type*, but with a *start type* of `0`: `T[any:Y] -> T[:Y][number][]`. + type T1 = [1, 2, 3][number:2]; + type T2 = [1, 2, 3][string:2]; + type T3 = [1, 2, 3][any:2]; + type T4 = [1, 2, 3][never:2]; + } + { + // - If the *end type* is neither a *String Literal Type* nor a *Numeric Literal Type*, then + // - Return an *Array Type* for the union of each element of the *Range Type* for the same *object type* and + // *start type*, but with an *end type* of `^0`: `T[X:any] -> T[X:][number][]`. + type T1 = [1, 2, 3][1:number]; + type T2 = [1, 2, 3][1:string]; + type T3 = [1, 2, 3][1:any]; + type T4 = [1, 2, 3][1:never]; + } + { + // - If the *start type* is the *lower bound type* (`0`) and the *end type* is the *upper bound type* (`^0`), + // then we are including all elements: return the *object type* of the *Range Type*. + type T1 = [1, 2, 3][0:^0]; + } + { + // - If the *start type* is the *upper bound type* (`^0`) or the *end type* is the *lower bound type* (`0`), + // then we are including no elements: return the empty *Tuple Type* (`[]`). + type T1 = [1, 2, 3][^0:^0]; + type T2 = [1, 2, 3][0:0]; + } + { + // - If the *object type* is an *Array Type*, then + { + // - If the signs of both the *start type* and *end type* agree, then return + // a fixed-length *Tuple Type* with a minimum length of `0` for the difference between the *end type* and + // the *start type* whose elements are the element type of the *object type*: `T[][0:1] -> [T?]`. + type T1 = number[][0:2]; + type T2 = number[][^2:^0]; + } + { + // - If the *start type* is an *Inverse Offset Type* and the *end type* is not, we can create a tuple of + // `min(abs(start), end)` optional elements. + type T1 = number[][^2:4]; + } + { + // - Otherwise, we cannot derive a fixed length: return the *object type* of the *Range Type*. + type T1 = number[][0:^2]; + } + } + { + // - Otherwise, the *object type* is a *Tuple Type*: + { + // - If the *object type* has a rest element, then + { + // - If the *start type* is an *Inverse Offset Type*, return an *Array Type* for the union of each + // *optional type* and the *rest element type*, along with the set of `n` right-most required + // elements of the *object type* where `n` is the absolute value of the *start type*. + type T1 = [1, 2, 3, ...4[]][^1:]; + type T2 = [1, 2, 3, ...4[]][^2:]; + type T3 = [1, 2, 3, ...4[]][^3:]; + type T4 = [1, 2, 3?, ...4[]][^1:]; + type T5 = [1, 2, 3?, ...4[]][^2:]; + type T6 = [1, 2, 3?, ...4[]][^3:]; + } + { + // - If the *end type* is an *Inverse Offset Type*, return a *Tuple Type* of the elements of the + // *object type* starting from the index at *start type*, but whose minimum length is reduced by the + // absolute value of the *end type*. + type T1 = [1, 2, 3, ...4[]][1:^1]; + type T2 = [1, 2, 3, ...4[]][1:^2]; + type T3 = [1, 2, 3, ...4[]][1:^3]; + type T4 = [1, 2, 3?, ...4[]][1:^1]; + type T5 = [1, 2, 3?, ...4[]][1:^2]; + type T6 = [1, 2, 3?, ...4[]][1:^3]; + } + } + { + // - Otherwise, + { + // - Clamp the *start type* and *end type* to values between `0` and the length of *object type*. + type T1 = [1, 2, 3][^5:10]; + } + { + // - Return a *Tuple Type* for the elements of *object type* starting at *start type* and ending at + // *end type*. + type T1 = [1, 2, 3][1: 2]; + } + } + } +} + +// @filename: assignability.ts +// - `S` is assignable to `T[X:Y]` if `S` is assignable to `C`, where `C` is the base constraint of `T[X:Y]` for writing. +function f1(t: T[U:^1], s: [3] & [2, 3]) { + t = s; +} + +// - `S[X:Y]` is assignable to `T[]` if `S[number]` is assignable to `T`. +function f2(t: T[], s: S[0:2]) { + t = s; +} + +// - `S[XS:YS]` is assignable to `T[XT:YT]` if `S` is assignable to `T`, `XS` is assignable to `XT`, and `YS` is assignable to `YT`. +function f3(t: T[1 | 2:3], s: S[1:3]) { + t = s; +} \ No newline at end of file