Skip to content

Mapped types allow numeric constraint types #18346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3119,7 +3119,7 @@ namespace ts {
}
}
if ((symbol as TransientSymbol).syntheticLiteralTypeOrigin) {
const stringValue = (symbol as TransientSymbol).syntheticLiteralTypeOrigin.value;
const stringValue = "" + (symbol as TransientSymbol).syntheticLiteralTypeOrigin.value;
if (!isIdentifierText(stringValue, compilerOptions.target)) {
return `"${escapeString(stringValue, CharacterCodes.doubleQuote)}"`;
}
Expand Down Expand Up @@ -5722,6 +5722,7 @@ namespace ts {
function resolveMappedTypeMembers(type: MappedType) {
const members: SymbolTable = createSymbolTable();
let stringIndexInfo: IndexInfo;
let numberIndexInfo: IndexInfo;
// Resolve upfront such that recursive references see an empty object type.
setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined);
// In { [P in K]: T }, we refer to P as the type parameter type, K as the constraint type,
Expand Down Expand Up @@ -5749,7 +5750,7 @@ namespace ts {
const iterationType = keyType.flags & TypeFlags.Index ? getIndexType(getApparentType((<IndexType>keyType).type)) : keyType;
forEachType(iterationType, addMemberForKeyType);
}
setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, undefined);
setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo);

function addMemberForKeyType(t: Type, propertySymbolOrIndex?: Symbol | number) {
let propertySymbol: Symbol;
Expand All @@ -5765,25 +5766,34 @@ namespace ts {
const iterationMapper = createTypeMapper([typeParameter], [t]);
const templateMapper = type.mapper ? combineTypeMappers(type.mapper, iterationMapper) : iterationMapper;
const propType = instantiateType(templateType, templateMapper);
// If the current iteration type constituent is a string literal type, create a property.
// If the current iteration type constituent is a string or number literal type, create a property.
// Otherwise, for type string create a string index signature.
if (t.flags & TypeFlags.StringLiteral) {
const propName = escapeLeadingUnderscores((<StringLiteralType>t).value);
const modifiersProp = getPropertyOfType(modifiersType, propName);
const isOptional = templateOptional || !!(modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName);
prop.checkFlags = templateReadonly || modifiersProp && isReadonlySymbol(modifiersProp) ? CheckFlags.Readonly : 0;
prop.type = propType;
if (propertySymbol) {
prop.syntheticOrigin = propertySymbol;
prop.declarations = propertySymbol.declarations;
}
prop.syntheticLiteralTypeOrigin = t as StringLiteralType;
members.set(propName, prop);
if (t.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
const propName = escapeLeadingUnderscores("" + (<StringLiteralType | NumberLiteralType>t).value);
if (members.has(propName)) {
// in case of a conflict between the literal types "1" and 1, for example, do not add either one
members.delete(propName);
}
else {
const modifiersProp = getPropertyOfType(modifiersType, propName);
const isOptional = templateOptional || !!(modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName);
prop.checkFlags = templateReadonly || modifiersProp && isReadonlySymbol(modifiersProp) ? CheckFlags.Readonly : 0;
prop.type = propType;
if (propertySymbol) {
prop.syntheticOrigin = propertySymbol;
prop.declarations = propertySymbol.declarations;
}
prop.syntheticLiteralTypeOrigin = t as StringLiteralType | NumberLiteralType;
members.set(propName, prop);
}
}
else if (t.flags & TypeFlags.String) {
stringIndexInfo = createIndexInfo(propType, templateReadonly);
}
else if (t.flags & (TypeFlags.Enum | TypeFlags.Number)) {
numberIndexInfo = createIndexInfo(propType, templateReadonly);
}
}
}

Expand Down Expand Up @@ -18856,7 +18866,7 @@ namespace ts {
checkSourceElement(node.type);
const type = <MappedType>getTypeFromMappedTypeNode(node);
const constraintType = getConstraintTypeFromMappedType(type);
checkTypeAssignableTo(constraintType, stringType, node.typeParameter.constraint);
checkTypeAssignableTo(constraintType, getUnionType([stringType, numberType]), node.typeParameter.constraint);
}

function isPrivateWithinAmbient(node: Node): boolean {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3033,7 +3033,7 @@ namespace ts {
leftSpread?: Symbol; // Left source for synthetic spread property
rightSpread?: Symbol; // Right source for synthetic spread property
syntheticOrigin?: Symbol; // For a property on a mapped or spread type, points back to the original property
syntheticLiteralTypeOrigin?: StringLiteralType; // For a property on a mapped type, indicates the type whose text to use as the declaration name, instead of the symbol name
syntheticLiteralTypeOrigin?: StringLiteralType | NumberLiteralType; // For a property on a mapped type, indicates the type whose text to use as the declaration name, instead of the symbol name
isDiscriminantProperty?: boolean; // True if discriminant synthetic property
resolvedExports?: SymbolTable; // Resolved exports of module
exportsChecked?: boolean; // True if exports of external module have been checked
Expand Down Expand Up @@ -3376,6 +3376,7 @@ namespace ts {
}

/* @internal */
/** Syntax maps to property names like so: { [typeParameter in constraintType]: templateType } */
export interface MappedType extends AnonymousType {
declaration: MappedTypeNode;
typeParameter?: TypeParameter;
Expand Down
17 changes: 9 additions & 8 deletions tests/baselines/reference/mappedTypeErrors.errors.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(19,20): error TS2313: Type parameter 'P' has a circular constraint.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(20,20): error TS2322: Type 'number' is not assignable to type 'string'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(21,20): error TS2322: Type 'Date' is not assignable to type 'string'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(21,20): error TS2322: Type 'Date' is not assignable to type 'string | number'.
Type 'Date' is not assignable to type 'number'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(22,19): error TS2344: Type 'Date' does not satisfy the constraint 'string'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(25,24): error TS2344: Type '"foo"' does not satisfy the constraint '"name" | "width" | "height" | "visible"'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(26,24): error TS2344: Type '"name" | "foo"' does not satisfy the constraint '"name" | "width" | "height" | "visible"'.
Expand Down Expand Up @@ -45,11 +45,12 @@ tests/cases/conformance/types/mapped/mappedTypeErrors.ts(129,5): error TS2322: T
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(130,5): error TS2322: Type '{ a: string; }' is not assignable to type '{ [x: string]: any; a?: number | undefined; }'.
Types of property 'a' are incompatible.
Type 'string' is not assignable to type 'number | undefined'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(136,16): error TS2322: Type 'T' is not assignable to type 'string'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(136,16): error TS2322: Type 'T' is not assignable to type 'string | number'.
Type 'T' is not assignable to type 'number'.
tests/cases/conformance/types/mapped/mappedTypeErrors.ts(136,21): error TS2536: Type 'P' cannot be used to index type 'T'.


==== tests/cases/conformance/types/mapped/mappedTypeErrors.ts (26 errors) ====
==== tests/cases/conformance/types/mapped/mappedTypeErrors.ts (25 errors) ====
interface Shape {
name: string;
width: number;
Expand All @@ -72,11 +73,10 @@ tests/cases/conformance/types/mapped/mappedTypeErrors.ts(136,21): error TS2536:
~
!!! error TS2313: Type parameter 'P' has a circular constraint.
type T01 = { [P in number]: string }; // Error
~~~~~~
!!! error TS2322: Type 'number' is not assignable to type 'string'.
type T02 = { [P in Date]: number }; // Error
~~~~
!!! error TS2322: Type 'Date' is not assignable to type 'string'.
!!! error TS2322: Type 'Date' is not assignable to type 'string | number'.
!!! error TS2322: Type 'Date' is not assignable to type 'number'.
type T03 = Record<Date, number>; // Error
~~~~
!!! error TS2344: Type 'Date' does not satisfy the constraint 'string'.
Expand Down Expand Up @@ -258,7 +258,8 @@ tests/cases/conformance/types/mapped/mappedTypeErrors.ts(136,21): error TS2536:
pf: {[P in F]?: T[P]},
pt: {[P in T]?: T[P]}, // note: should be in keyof T
~
!!! error TS2322: Type 'T' is not assignable to type 'string'.
!!! error TS2322: Type 'T' is not assignable to type 'string | number'.
!!! error TS2322: Type 'T' is not assignable to type 'number'.
~~~~
!!! error TS2536: Type 'P' cannot be used to index type 'T'.
};
Expand Down
127 changes: 127 additions & 0 deletions tests/baselines/reference/mappedTypeNumericEnum.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(13,35): error TS2322: Type '{ '0': true; '2': boolean; }' is not assignable to type 'NumBool'.
Object literal may only specify known properties, and ''2'' does not exist in type 'NumBool'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(14,5): error TS2322: Type '{ '0': true; }' is not assignable to type 'NumBool'.
Property '"1"' is missing in type '{ '0': true; }'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(17,1): error TS7017: Element implicitly has an 'any' type because type 'NumBool' has no index signature.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(27,1): error TS7017: Element implicitly has an 'any' type because type 'StrAny' has no index signature.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(33,49): error TS2322: Type '{ '0': number; [Nums.B]: number; [Nums2.Gimel]: number; }' is not assignable to type 'NumNum'.
Object literal may only specify known properties, and '[Nums2.Gimel]' does not exist in type 'NumNum'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(41,40): error TS2322: Type '{ [Nums.A]: number; [Nums.B]: number; }' is not assignable to type 'OneNumNum'.
Object literal may only specify known properties, and '[Nums.B]' does not exist in type 'OneNumNum'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(44,1): error TS7017: Element implicitly has an 'any' type because type 'OneNumNum' has no index signature.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(50,6): error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(51,6): error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(63,4): error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(69,5): error TS2322: Type '{ [0]: number; [1]: number; }' is not assignable to type 'MixNum'.
Property 'a' is missing in type '{ [0]: number; [1]: number; }'.
tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts(75,30): error TS2322: Type '{ [0]: number; [1]: number; [2]: number; }' is not assignable to type 'MixConflictNum'.
Object literal may only specify known properties, and '[0]' does not exist in type 'MixConflictNum'.


==== tests/cases/conformance/types/mapped/mappedTypeNumericEnum.ts (12 errors) ====
// with numbers
enum Nums {
A,
B
}
enum Nums2 {
Aleph,
Bet,
Gimel
}
type NumBool = { [K in Nums]: boolean }
let nb: NumBool = { '0': true, '1': false }
let wronb: NumBool = { '0': true, '2': false }
~~~~~~~~~~
!!! error TS2322: Type '{ '0': true; '2': boolean; }' is not assignable to type 'NumBool'.
!!! error TS2322: Object literal may only specify known properties, and ''2'' does not exist in type 'NumBool'.
let wronb2: NumBool = { '0': true }
~~~~~~
!!! error TS2322: Type '{ '0': true; }' is not assignable to type 'NumBool'.
!!! error TS2322: Property '"1"' is missing in type '{ '0': true; }'.
nb[Nums.A] = false;
nb[Nums2.Bet] = true;
nb[Nums2.Gimel] = false; // only disallowed with --strict
~~~~~~~~~~~~~~~
!!! error TS7017: Element implicitly has an 'any' type because type 'NumBool' has no index signature.

// with strings
enum Strs {
A = 'a',
B = 'b'
}
type StrAny = { [K in Strs]: any }
let sa: StrAny = { a: 1, b: 2 }
sa[Strs.A] = 'a'
sa['nope'] = 'not allowed'
~~~~~~~~~~
!!! error TS7017: Element implicitly has an 'any' type because type 'StrAny' has no index signature.

// union of numbers
type Ns = 0 | 1;
type NumNum = { [K in Ns]: number }
let nn: NumNum = { [Nums.A]: 3, [Nums.B]: 4 }
let omnomnom: NumNum = { '0': 12, [Nums.B]: 13, [Nums2.Gimel]: 14 }
~~~~~~~~~~~~~~~~~
!!! error TS2322: Type '{ '0': number; [Nums.B]: number; [Nums2.Gimel]: number; }' is not assignable to type 'NumNum'.
!!! error TS2322: Object literal may only specify known properties, and '[Nums2.Gimel]' does not exist in type 'NumNum'.
nn[0] = 5
nn['1'] = 6

// single number
type N = 0;
type OneNumNum = { [K in N]: number }
let onn: OneNumNum = { [Nums.A]: 7 }
let wronng: OneNumNum = { [Nums.A]: 7, [Nums.B]: 11 }
~~~~~~~~~~~~
!!! error TS2322: Type '{ [Nums.A]: number; [Nums.B]: number; }' is not assignable to type 'OneNumNum'.
!!! error TS2322: Object literal may only specify known properties, and '[Nums.B]' does not exist in type 'OneNumNum'.
onn[0] = 8
onn['0'] = 9
onn[1] = 10 // only disallowed with --strict
~~~~~~
!!! error TS7017: Element implicitly has an 'any' type because type 'OneNumNum' has no index signature.

// just number
type NumberNum = { [K in number]: number }
let numn: NumberNum = { }
numn[0] = 31
numn['1'] = 32
~~~
!!! error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.
numn['oops'] = 33
~~~~~~
!!! error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.

// computed enum gets a string indexer
enum Comp {
A,
B = 1 << 3
}

type CompNum = { [K in Comp]: number }
let cn: CompNum = { [Comp.A]: 15 }
let cnn: CompNum = { [Comp.A]: 16, '101': 17 }
cn[1001] = 18
cn['maybe?'] = 19
~~~~~~~~
!!! error TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.

// manual string/number union mixes
type Mix = 0 | 1 | 'a' | 'i' | 'u';
type MixNum = { [K in Mix]: number }
let mn: MixNum = { [0]: 20, '1': 21, a: 22, i: 23, u: 24 }
let mnn: MixNum = { [0]: 29, [1]: 30 }
~~~
!!! error TS2322: Type '{ [0]: number; [1]: number; }' is not assignable to type 'MixNum'.
!!! error TS2322: Property 'a' is missing in type '{ [0]: number; [1]: number; }'.

// conflicts result in the property being thrown out
type MixConflict = 0 | 1 | 1 | 1 | 1 | 1 | 2 | '0' | '1';
type MixConflictNum = { [K in MixConflict]: number }
let mcn: MixConflictNum = { [2]: 25 }
let mcnn: MixConflictNum = { [0]: 26, [1]: 27, [2]: 28 }
~~~~~~~
!!! error TS2322: Type '{ [0]: number; [1]: number; [2]: number; }' is not assignable to type 'MixConflictNum'.
!!! error TS2322: Object literal may only specify known properties, and '[0]' does not exist in type 'MixConflictNum'.

Loading