Skip to content

Commit d06a592

Browse files
authoredOct 7, 2022
Properly defer resolution of mapped types with generic as clauses (#51050)
* Fix isGenericMappedType, getTemplateLiteralType, getStringMappingType * Accept new baselines * Add regression tests * Fix comment
1 parent 42b1049 commit d06a592

12 files changed

+299
-30
lines changed
 

‎src/compiler/checker.ts

+31-9
Original file line numberDiff line numberDiff line change
@@ -12073,7 +12073,20 @@ namespace ts {
1207312073
}
1207412074

1207512075
function isGenericMappedType(type: Type): type is MappedType {
12076-
return !!(getObjectFlags(type) & ObjectFlags.Mapped) && isGenericIndexType(getConstraintTypeFromMappedType(type as MappedType));
12076+
if (getObjectFlags(type) & ObjectFlags.Mapped) {
12077+
const constraint = getConstraintTypeFromMappedType(type as MappedType);
12078+
if (isGenericIndexType(constraint)) {
12079+
return true;
12080+
}
12081+
// A mapped type is generic if the 'as' clause references generic types other than the iteration type.
12082+
// To determine this, we substitute the constraint type (that we now know isn't generic) for the iteration
12083+
// type and check whether the resulting type is generic.
12084+
const nameType = getNameTypeFromMappedType(type as MappedType);
12085+
if (nameType && isGenericIndexType(instantiateType(nameType, makeUnaryTypeMapper(getTypeParameterFromMappedType(type as MappedType), constraint)))) {
12086+
return true;
12087+
}
12088+
}
12089+
return false;
1207712090
}
1207812091

1207912092
function resolveStructuredTypeMembers(type: StructuredType): ResolvedType {
@@ -15638,8 +15651,14 @@ namespace ts {
1563815651
return getStringLiteralType(text);
1563915652
}
1564015653
newTexts.push(text);
15641-
if (every(newTexts, t => t === "") && every(newTypes, t => !!(t.flags & TypeFlags.String))) {
15642-
return stringType;
15654+
if (every(newTexts, t => t === "")) {
15655+
if (every(newTypes, t => !!(t.flags & TypeFlags.String))) {
15656+
return stringType;
15657+
}
15658+
// Normalize `${Mapping<xxx>}` into Mapping<xxx>
15659+
if (newTypes.length === 1 && isPatternLiteralType(newTypes[0])) {
15660+
return newTypes[0];
15661+
}
1564315662
}
1564415663
const id = `${getTypeListId(newTypes)}|${map(newTexts, t => t.length).join(",")}|${newTexts.join("")}`;
1564515664
let type = templateLiteralTypes.get(id);
@@ -15698,11 +15717,13 @@ namespace ts {
1569815717

1569915718
function getStringMappingType(symbol: Symbol, type: Type): Type {
1570015719
return type.flags & (TypeFlags.Union | TypeFlags.Never) ? mapType(type, t => getStringMappingType(symbol, t)) :
15701-
// Mapping<Mapping<T>> === Mapping<T>
15702-
type.flags & TypeFlags.StringMapping && symbol === type.symbol ? type :
15703-
isGenericIndexType(type) || isPatternLiteralPlaceholderType(type) ? getStringMappingTypeForGenericType(symbol, isPatternLiteralPlaceholderType(type) && !(type.flags & TypeFlags.StringMapping) ? getTemplateLiteralType(["", ""], [type]) : type) :
1570415720
type.flags & TypeFlags.StringLiteral ? getStringLiteralType(applyStringMapping(symbol, (type as StringLiteralType).value)) :
1570515721
type.flags & TypeFlags.TemplateLiteral ? getTemplateLiteralType(...applyTemplateStringMapping(symbol, (type as TemplateLiteralType).texts, (type as TemplateLiteralType).types)) :
15722+
// Mapping<Mapping<T>> === Mapping<T>
15723+
type.flags & TypeFlags.StringMapping && symbol === type.symbol ? type :
15724+
type.flags & (TypeFlags.Any | TypeFlags.String || type.flags & TypeFlags.StringMapping) || isGenericIndexType(type) ? getStringMappingTypeForGenericType(symbol, type) :
15725+
// This handles Mapping<`${number}`> and Mapping<`${bigint}`>
15726+
isPatternLiteralPlaceholderType(type) ? getStringMappingTypeForGenericType(symbol, getTemplateLiteralType(["", ""], [type])) :
1570615727
type;
1570715728
}
1570815729

@@ -16000,11 +16021,12 @@ namespace ts {
1600016021
}
1600116022

1600216023
function isPatternLiteralPlaceholderType(type: Type): boolean {
16003-
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt)) || !!(type.flags & TypeFlags.StringMapping && isPatternLiteralPlaceholderType((type as StringMappingType).type));
16024+
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt)) || isPatternLiteralType(type);
1600416025
}
1600516026

1600616027
function isPatternLiteralType(type: Type) {
16007-
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, isPatternLiteralPlaceholderType);
16028+
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, isPatternLiteralPlaceholderType) ||
16029+
!!(type.flags & TypeFlags.StringMapping) && isPatternLiteralPlaceholderType((type as StringMappingType).type);
1600816030
}
1600916031

1601016032
function isGenericType(type: Type): boolean {
@@ -22559,7 +22581,7 @@ namespace ts {
2255922581
}
2256022582

2256122583
function isMemberOfStringMapping(source: Type, target: Type): boolean {
22562-
if (target.flags & (TypeFlags.String | TypeFlags.AnyOrUnknown)) {
22584+
if (target.flags & (TypeFlags.String | TypeFlags.Any)) {
2256322585
return true;
2256422586
}
2256522587
if (target.flags & TypeFlags.TemplateLiteral) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
tests/cases/compiler/genericMappedTypeAsClause.ts(11,36): error TS2322: Type 'string' is not assignable to type 'number'.
2+
tests/cases/compiler/genericMappedTypeAsClause.ts(14,11): error TS2322: Type 'number' is not assignable to type 'MappedModel<T>'.
3+
tests/cases/compiler/genericMappedTypeAsClause.ts(15,11): error TS2322: Type 'string' is not assignable to type 'MappedModel<T>'.
4+
tests/cases/compiler/genericMappedTypeAsClause.ts(16,11): error TS2322: Type 'number[]' is not assignable to type 'MappedModel<T>'.
5+
tests/cases/compiler/genericMappedTypeAsClause.ts(17,11): error TS2322: Type 'boolean' is not assignable to type 'MappedModel<T>'.
6+
tests/cases/compiler/genericMappedTypeAsClause.ts(18,34): error TS2322: Type '{ a: string; b: number; }' is not assignable to type 'MappedModel<T>'.
7+
Object literal may only specify known properties, and 'a' does not exist in type 'MappedModel<T>'.
8+
tests/cases/compiler/genericMappedTypeAsClause.ts(19,11): error TS2322: Type 'undefined' is not assignable to type 'MappedModel<T>'.
9+
10+
11+
==== tests/cases/compiler/genericMappedTypeAsClause.ts (7 errors) ====
12+
type Model = {
13+
a: string;
14+
b: number;
15+
};
16+
17+
type MappedModel<Suffix extends string> = {
18+
[K in keyof Model as `${K}${Suffix}`]: Model[K];
19+
};
20+
21+
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
22+
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
23+
~~~~
24+
!!! error TS2322: Type 'string' is not assignable to type 'number'.
25+
!!! related TS6500 tests/cases/compiler/genericMappedTypeAsClause.ts:6:43: The expected type comes from property 'bFoo' which is declared here on type 'MappedModel<"Foo">'
26+
27+
function f1<T extends string>() {
28+
const x1: MappedModel<T> = 42; // Error
29+
~~
30+
!!! error TS2322: Type 'number' is not assignable to type 'MappedModel<T>'.
31+
const x2: MappedModel<T> = 'test'; // Error
32+
~~
33+
!!! error TS2322: Type 'string' is not assignable to type 'MappedModel<T>'.
34+
const x3: MappedModel<T> = [1, 2, 3]; // Error
35+
~~
36+
!!! error TS2322: Type 'number[]' is not assignable to type 'MappedModel<T>'.
37+
const x4: MappedModel<T> = false; // Error
38+
~~
39+
!!! error TS2322: Type 'boolean' is not assignable to type 'MappedModel<T>'.
40+
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
41+
~~~~~~~~
42+
!!! error TS2322: Type '{ a: string; b: number; }' is not assignable to type 'MappedModel<T>'.
43+
!!! error TS2322: Object literal may only specify known properties, and 'a' does not exist in type 'MappedModel<T>'.
44+
const x6: MappedModel<T> = undefined; // Error
45+
~~
46+
!!! error TS2322: Type 'undefined' is not assignable to type 'MappedModel<T>'.
47+
}
48+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//// [genericMappedTypeAsClause.ts]
2+
type Model = {
3+
a: string;
4+
b: number;
5+
};
6+
7+
type MappedModel<Suffix extends string> = {
8+
[K in keyof Model as `${K}${Suffix}`]: Model[K];
9+
};
10+
11+
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
12+
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
13+
14+
function f1<T extends string>() {
15+
const x1: MappedModel<T> = 42; // Error
16+
const x2: MappedModel<T> = 'test'; // Error
17+
const x3: MappedModel<T> = [1, 2, 3]; // Error
18+
const x4: MappedModel<T> = false; // Error
19+
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
20+
const x6: MappedModel<T> = undefined; // Error
21+
}
22+
23+
24+
//// [genericMappedTypeAsClause.js]
25+
"use strict";
26+
var foo1 = { aFoo: 'test', bFoo: 42 };
27+
var foo2 = { bFoo: 'bar' }; // Error
28+
function f1() {
29+
var x1 = 42; // Error
30+
var x2 = 'test'; // Error
31+
var x3 = [1, 2, 3]; // Error
32+
var x4 = false; // Error
33+
var x5 = { a: 'bar', b: 42 }; // Error
34+
var x6 = undefined; // Error
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
=== tests/cases/compiler/genericMappedTypeAsClause.ts ===
2+
type Model = {
3+
>Model : Symbol(Model, Decl(genericMappedTypeAsClause.ts, 0, 0))
4+
5+
a: string;
6+
>a : Symbol(a, Decl(genericMappedTypeAsClause.ts, 0, 14))
7+
8+
b: number;
9+
>b : Symbol(b, Decl(genericMappedTypeAsClause.ts, 1, 14))
10+
11+
};
12+
13+
type MappedModel<Suffix extends string> = {
14+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
15+
>Suffix : Symbol(Suffix, Decl(genericMappedTypeAsClause.ts, 5, 17))
16+
17+
[K in keyof Model as `${K}${Suffix}`]: Model[K];
18+
>K : Symbol(K, Decl(genericMappedTypeAsClause.ts, 6, 5))
19+
>Model : Symbol(Model, Decl(genericMappedTypeAsClause.ts, 0, 0))
20+
>K : Symbol(K, Decl(genericMappedTypeAsClause.ts, 6, 5))
21+
>Suffix : Symbol(Suffix, Decl(genericMappedTypeAsClause.ts, 5, 17))
22+
>Model : Symbol(Model, Decl(genericMappedTypeAsClause.ts, 0, 0))
23+
>K : Symbol(K, Decl(genericMappedTypeAsClause.ts, 6, 5))
24+
25+
};
26+
27+
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
28+
>foo1 : Symbol(foo1, Decl(genericMappedTypeAsClause.ts, 9, 5))
29+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
30+
>aFoo : Symbol(aFoo, Decl(genericMappedTypeAsClause.ts, 9, 34))
31+
>bFoo : Symbol(bFoo, Decl(genericMappedTypeAsClause.ts, 9, 48))
32+
33+
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
34+
>foo2 : Symbol(foo2, Decl(genericMappedTypeAsClause.ts, 10, 5))
35+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
36+
>bFoo : Symbol(bFoo, Decl(genericMappedTypeAsClause.ts, 10, 34))
37+
38+
function f1<T extends string>() {
39+
>f1 : Symbol(f1, Decl(genericMappedTypeAsClause.ts, 10, 49))
40+
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
41+
42+
const x1: MappedModel<T> = 42; // Error
43+
>x1 : Symbol(x1, Decl(genericMappedTypeAsClause.ts, 13, 9))
44+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
45+
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
46+
47+
const x2: MappedModel<T> = 'test'; // Error
48+
>x2 : Symbol(x2, Decl(genericMappedTypeAsClause.ts, 14, 9))
49+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
50+
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
51+
52+
const x3: MappedModel<T> = [1, 2, 3]; // Error
53+
>x3 : Symbol(x3, Decl(genericMappedTypeAsClause.ts, 15, 9))
54+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
55+
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
56+
57+
const x4: MappedModel<T> = false; // Error
58+
>x4 : Symbol(x4, Decl(genericMappedTypeAsClause.ts, 16, 9))
59+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
60+
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
61+
62+
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
63+
>x5 : Symbol(x5, Decl(genericMappedTypeAsClause.ts, 17, 9))
64+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
65+
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
66+
>a : Symbol(a, Decl(genericMappedTypeAsClause.ts, 17, 32))
67+
>b : Symbol(b, Decl(genericMappedTypeAsClause.ts, 17, 42))
68+
69+
const x6: MappedModel<T> = undefined; // Error
70+
>x6 : Symbol(x6, Decl(genericMappedTypeAsClause.ts, 18, 9))
71+
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
72+
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
73+
>undefined : Symbol(undefined)
74+
}
75+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
=== tests/cases/compiler/genericMappedTypeAsClause.ts ===
2+
type Model = {
3+
>Model : { a: string; b: number; }
4+
5+
a: string;
6+
>a : string
7+
8+
b: number;
9+
>b : number
10+
11+
};
12+
13+
type MappedModel<Suffix extends string> = {
14+
>MappedModel : MappedModel<Suffix>
15+
16+
[K in keyof Model as `${K}${Suffix}`]: Model[K];
17+
};
18+
19+
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
20+
>foo1 : MappedModel<"Foo">
21+
>{ aFoo: 'test', bFoo: 42 } : { aFoo: string; bFoo: number; }
22+
>aFoo : string
23+
>'test' : "test"
24+
>bFoo : number
25+
>42 : 42
26+
27+
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
28+
>foo2 : MappedModel<"Foo">
29+
>{ bFoo: 'bar' } : { bFoo: string; }
30+
>bFoo : string
31+
>'bar' : "bar"
32+
33+
function f1<T extends string>() {
34+
>f1 : <T extends string>() => void
35+
36+
const x1: MappedModel<T> = 42; // Error
37+
>x1 : MappedModel<T>
38+
>42 : 42
39+
40+
const x2: MappedModel<T> = 'test'; // Error
41+
>x2 : MappedModel<T>
42+
>'test' : "test"
43+
44+
const x3: MappedModel<T> = [1, 2, 3]; // Error
45+
>x3 : MappedModel<T>
46+
>[1, 2, 3] : number[]
47+
>1 : 1
48+
>2 : 2
49+
>3 : 3
50+
51+
const x4: MappedModel<T> = false; // Error
52+
>x4 : MappedModel<T>
53+
>false : false
54+
55+
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
56+
>x5 : MappedModel<T>
57+
>{ a: 'bar', b: 42 } : { a: string; b: number; }
58+
>a : string
59+
>'bar' : "bar"
60+
>b : number
61+
>42 : 42
62+
63+
const x6: MappedModel<T> = undefined; // Error
64+
>x6 : MappedModel<T>
65+
>undefined : undefined
66+
}
67+

‎tests/baselines/reference/intrinsicTypes.types

+6-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type TU3 = Uppercase<string>; // Uppercase<string>
99
>TU3 : Uppercase<string>
1010

1111
type TU4 = Uppercase<any>; // Uppercase<`${any}`>
12-
>TU4 : Uppercase<`${any}`>
12+
>TU4 : Uppercase<any>
1313

1414
type TU5 = Uppercase<never>; // never
1515
>TU5 : never
@@ -27,7 +27,7 @@ type TL3 = Lowercase<string>; // Lowercase<string>
2727
>TL3 : Lowercase<string>
2828

2929
type TL4 = Lowercase<any>; // Lowercase<`${any}`>
30-
>TL4 : Lowercase<`${any}`>
30+
>TL4 : Lowercase<any>
3131

3232
type TL5 = Lowercase<never>; // never
3333
>TL5 : never
@@ -45,7 +45,7 @@ type TC3 = Capitalize<string>; // Capitalize<string>
4545
>TC3 : Capitalize<string>
4646

4747
type TC4 = Capitalize<any>; // Capitalize<`${any}`>
48-
>TC4 : Capitalize<`${any}`>
48+
>TC4 : Capitalize<any>
4949

5050
type TC5 = Capitalize<never>; // never
5151
>TC5 : never
@@ -63,7 +63,7 @@ type TN3 = Uncapitalize<string>; // Uncapitalize<string>
6363
>TN3 : Uncapitalize<string>
6464

6565
type TN4 = Uncapitalize<any>; // Uncapitalize<`${any}`>
66-
>TN4 : Uncapitalize<`${any}`>
66+
>TN4 : Uncapitalize<any>
6767

6868
type TN5 = Uncapitalize<never>; // never
6969
>TN5 : never
@@ -72,13 +72,13 @@ type TN6 = Uncapitalize<42>; // Error
7272
>TN6 : 42
7373

7474
type TX1<S extends string> = Uppercase<`aB${S}`>;
75-
>TX1 : Uppercase<`aB${S}`>
75+
>TX1 : `AB${Uppercase<S>}`
7676

7777
type TX2 = TX1<'xYz'>; // "ABXYZ"
7878
>TX2 : "ABXYZ"
7979

8080
type TX3<S extends string> = Lowercase<`aB${S}`>;
81-
>TX3 : Lowercase<`aB${S}`>
81+
>TX3 : `ab${Lowercase<S>}`
8282

8383
type TX4 = TX3<'xYz'>; // "abxyz"
8484
>TX4 : "abxyz"

‎tests/baselines/reference/mappedTypeAsClauses.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ type TD1 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b
6161
>b : number
6262

6363
type TD2 = keyof TD1; // 'a1' | 'a2' | 'b1' | 'b2'
64-
>TD2 : "a1" | "a2" | "b1" | "b2"
64+
>TD2 : "a1" | "b1" | "a2" | "b2"
6565

6666
type TD3<U> = keyof DoubleProp<U>; // `${keyof U & string}1` | `${keyof U & string}2`
6767
>TD3 : `${keyof U & string}1` | `${keyof U & string}2`

‎tests/baselines/reference/numericStringLiteralTypes.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type T3<T extends string> = string & `${T}`; // `${T}
1212
>T3 : `${T}`
1313

1414
type T4<T extends string> = string & `${Capitalize<`${T}`>}`; // `${Capitalize<T>}`
15-
>T4 : `${Capitalize<`${T}`>}`
15+
>T4 : `${Capitalize<T>}`
1616

1717
function f1(a: boolean[], x: `${number}`) {
1818
>f1 : (a: boolean[], x: `${number}`) => void

0 commit comments

Comments
 (0)
Please sign in to comment.