Properly defer resolution of mapped types with generic as clauses (#51050)

* Fix isGenericMappedType, getTemplateLiteralType, getStringMappingType

* Accept new baselines

* Add regression tests

* Fix comment
This commit is contained in:
Anders Hejlsberg 2022-10-07 07:25:57 -07:00 committed by GitHub
parent 42b1049aee
commit d06a592d02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 299 additions and 30 deletions

View File

@ -12073,7 +12073,20 @@ namespace ts {
}
function isGenericMappedType(type: Type): type is MappedType {
return !!(getObjectFlags(type) & ObjectFlags.Mapped) && isGenericIndexType(getConstraintTypeFromMappedType(type as MappedType));
if (getObjectFlags(type) & ObjectFlags.Mapped) {
const constraint = getConstraintTypeFromMappedType(type as MappedType);
if (isGenericIndexType(constraint)) {
return true;
}
// A mapped type is generic if the 'as' clause references generic types other than the iteration type.
// To determine this, we substitute the constraint type (that we now know isn't generic) for the iteration
// type and check whether the resulting type is generic.
const nameType = getNameTypeFromMappedType(type as MappedType);
if (nameType && isGenericIndexType(instantiateType(nameType, makeUnaryTypeMapper(getTypeParameterFromMappedType(type as MappedType), constraint)))) {
return true;
}
}
return false;
}
function resolveStructuredTypeMembers(type: StructuredType): ResolvedType {
@ -15638,8 +15651,14 @@ namespace ts {
return getStringLiteralType(text);
}
newTexts.push(text);
if (every(newTexts, t => t === "") && every(newTypes, t => !!(t.flags & TypeFlags.String))) {
return stringType;
if (every(newTexts, t => t === "")) {
if (every(newTypes, t => !!(t.flags & TypeFlags.String))) {
return stringType;
}
// Normalize `${Mapping<xxx>}` into Mapping<xxx>
if (newTypes.length === 1 && isPatternLiteralType(newTypes[0])) {
return newTypes[0];
}
}
const id = `${getTypeListId(newTypes)}|${map(newTexts, t => t.length).join(",")}|${newTexts.join("")}`;
let type = templateLiteralTypes.get(id);
@ -15698,11 +15717,13 @@ namespace ts {
function getStringMappingType(symbol: Symbol, type: Type): Type {
return type.flags & (TypeFlags.Union | TypeFlags.Never) ? mapType(type, t => getStringMappingType(symbol, t)) :
// Mapping<Mapping<T>> === Mapping<T>
type.flags & TypeFlags.StringMapping && symbol === type.symbol ? type :
isGenericIndexType(type) || isPatternLiteralPlaceholderType(type) ? getStringMappingTypeForGenericType(symbol, isPatternLiteralPlaceholderType(type) && !(type.flags & TypeFlags.StringMapping) ? getTemplateLiteralType(["", ""], [type]) : type) :
type.flags & TypeFlags.StringLiteral ? getStringLiteralType(applyStringMapping(symbol, (type as StringLiteralType).value)) :
type.flags & TypeFlags.TemplateLiteral ? getTemplateLiteralType(...applyTemplateStringMapping(symbol, (type as TemplateLiteralType).texts, (type as TemplateLiteralType).types)) :
// Mapping<Mapping<T>> === Mapping<T>
type.flags & TypeFlags.StringMapping && symbol === type.symbol ? type :
type.flags & (TypeFlags.Any | TypeFlags.String || type.flags & TypeFlags.StringMapping) || isGenericIndexType(type) ? getStringMappingTypeForGenericType(symbol, type) :
// This handles Mapping<`${number}`> and Mapping<`${bigint}`>
isPatternLiteralPlaceholderType(type) ? getStringMappingTypeForGenericType(symbol, getTemplateLiteralType(["", ""], [type])) :
type;
}
@ -16000,11 +16021,12 @@ namespace ts {
}
function isPatternLiteralPlaceholderType(type: Type): boolean {
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt)) || !!(type.flags & TypeFlags.StringMapping && isPatternLiteralPlaceholderType((type as StringMappingType).type));
return !!(type.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.BigInt)) || isPatternLiteralType(type);
}
function isPatternLiteralType(type: Type) {
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, isPatternLiteralPlaceholderType);
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, isPatternLiteralPlaceholderType) ||
!!(type.flags & TypeFlags.StringMapping) && isPatternLiteralPlaceholderType((type as StringMappingType).type);
}
function isGenericType(type: Type): boolean {
@ -22559,7 +22581,7 @@ namespace ts {
}
function isMemberOfStringMapping(source: Type, target: Type): boolean {
if (target.flags & (TypeFlags.String | TypeFlags.AnyOrUnknown)) {
if (target.flags & (TypeFlags.String | TypeFlags.Any)) {
return true;
}
if (target.flags & TypeFlags.TemplateLiteral) {

View File

@ -0,0 +1,48 @@
tests/cases/compiler/genericMappedTypeAsClause.ts(11,36): error TS2322: Type 'string' is not assignable to type 'number'.
tests/cases/compiler/genericMappedTypeAsClause.ts(14,11): error TS2322: Type 'number' is not assignable to type 'MappedModel<T>'.
tests/cases/compiler/genericMappedTypeAsClause.ts(15,11): error TS2322: Type 'string' is not assignable to type 'MappedModel<T>'.
tests/cases/compiler/genericMappedTypeAsClause.ts(16,11): error TS2322: Type 'number[]' is not assignable to type 'MappedModel<T>'.
tests/cases/compiler/genericMappedTypeAsClause.ts(17,11): error TS2322: Type 'boolean' is not assignable to type 'MappedModel<T>'.
tests/cases/compiler/genericMappedTypeAsClause.ts(18,34): error TS2322: Type '{ a: string; b: number; }' is not assignable to type 'MappedModel<T>'.
Object literal may only specify known properties, and 'a' does not exist in type 'MappedModel<T>'.
tests/cases/compiler/genericMappedTypeAsClause.ts(19,11): error TS2322: Type 'undefined' is not assignable to type 'MappedModel<T>'.
==== tests/cases/compiler/genericMappedTypeAsClause.ts (7 errors) ====
type Model = {
a: string;
b: number;
};
type MappedModel<Suffix extends string> = {
[K in keyof Model as `${K}${Suffix}`]: Model[K];
};
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
~~~~
!!! error TS2322: Type 'string' is not assignable to type 'number'.
!!! related TS6500 tests/cases/compiler/genericMappedTypeAsClause.ts:6:43: The expected type comes from property 'bFoo' which is declared here on type 'MappedModel<"Foo">'
function f1<T extends string>() {
const x1: MappedModel<T> = 42; // Error
~~
!!! error TS2322: Type 'number' is not assignable to type 'MappedModel<T>'.
const x2: MappedModel<T> = 'test'; // Error
~~
!!! error TS2322: Type 'string' is not assignable to type 'MappedModel<T>'.
const x3: MappedModel<T> = [1, 2, 3]; // Error
~~
!!! error TS2322: Type 'number[]' is not assignable to type 'MappedModel<T>'.
const x4: MappedModel<T> = false; // Error
~~
!!! error TS2322: Type 'boolean' is not assignable to type 'MappedModel<T>'.
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
~~~~~~~~
!!! error TS2322: Type '{ a: string; b: number; }' is not assignable to type 'MappedModel<T>'.
!!! error TS2322: Object literal may only specify known properties, and 'a' does not exist in type 'MappedModel<T>'.
const x6: MappedModel<T> = undefined; // Error
~~
!!! error TS2322: Type 'undefined' is not assignable to type 'MappedModel<T>'.
}

View File

@ -0,0 +1,35 @@
//// [genericMappedTypeAsClause.ts]
type Model = {
a: string;
b: number;
};
type MappedModel<Suffix extends string> = {
[K in keyof Model as `${K}${Suffix}`]: Model[K];
};
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
function f1<T extends string>() {
const x1: MappedModel<T> = 42; // Error
const x2: MappedModel<T> = 'test'; // Error
const x3: MappedModel<T> = [1, 2, 3]; // Error
const x4: MappedModel<T> = false; // Error
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
const x6: MappedModel<T> = undefined; // Error
}
//// [genericMappedTypeAsClause.js]
"use strict";
var foo1 = { aFoo: 'test', bFoo: 42 };
var foo2 = { bFoo: 'bar' }; // Error
function f1() {
var x1 = 42; // Error
var x2 = 'test'; // Error
var x3 = [1, 2, 3]; // Error
var x4 = false; // Error
var x5 = { a: 'bar', b: 42 }; // Error
var x6 = undefined; // Error
}

View File

@ -0,0 +1,75 @@
=== tests/cases/compiler/genericMappedTypeAsClause.ts ===
type Model = {
>Model : Symbol(Model, Decl(genericMappedTypeAsClause.ts, 0, 0))
a: string;
>a : Symbol(a, Decl(genericMappedTypeAsClause.ts, 0, 14))
b: number;
>b : Symbol(b, Decl(genericMappedTypeAsClause.ts, 1, 14))
};
type MappedModel<Suffix extends string> = {
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>Suffix : Symbol(Suffix, Decl(genericMappedTypeAsClause.ts, 5, 17))
[K in keyof Model as `${K}${Suffix}`]: Model[K];
>K : Symbol(K, Decl(genericMappedTypeAsClause.ts, 6, 5))
>Model : Symbol(Model, Decl(genericMappedTypeAsClause.ts, 0, 0))
>K : Symbol(K, Decl(genericMappedTypeAsClause.ts, 6, 5))
>Suffix : Symbol(Suffix, Decl(genericMappedTypeAsClause.ts, 5, 17))
>Model : Symbol(Model, Decl(genericMappedTypeAsClause.ts, 0, 0))
>K : Symbol(K, Decl(genericMappedTypeAsClause.ts, 6, 5))
};
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
>foo1 : Symbol(foo1, Decl(genericMappedTypeAsClause.ts, 9, 5))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>aFoo : Symbol(aFoo, Decl(genericMappedTypeAsClause.ts, 9, 34))
>bFoo : Symbol(bFoo, Decl(genericMappedTypeAsClause.ts, 9, 48))
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
>foo2 : Symbol(foo2, Decl(genericMappedTypeAsClause.ts, 10, 5))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>bFoo : Symbol(bFoo, Decl(genericMappedTypeAsClause.ts, 10, 34))
function f1<T extends string>() {
>f1 : Symbol(f1, Decl(genericMappedTypeAsClause.ts, 10, 49))
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
const x1: MappedModel<T> = 42; // Error
>x1 : Symbol(x1, Decl(genericMappedTypeAsClause.ts, 13, 9))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
const x2: MappedModel<T> = 'test'; // Error
>x2 : Symbol(x2, Decl(genericMappedTypeAsClause.ts, 14, 9))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
const x3: MappedModel<T> = [1, 2, 3]; // Error
>x3 : Symbol(x3, Decl(genericMappedTypeAsClause.ts, 15, 9))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
const x4: MappedModel<T> = false; // Error
>x4 : Symbol(x4, Decl(genericMappedTypeAsClause.ts, 16, 9))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
>x5 : Symbol(x5, Decl(genericMappedTypeAsClause.ts, 17, 9))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
>a : Symbol(a, Decl(genericMappedTypeAsClause.ts, 17, 32))
>b : Symbol(b, Decl(genericMappedTypeAsClause.ts, 17, 42))
const x6: MappedModel<T> = undefined; // Error
>x6 : Symbol(x6, Decl(genericMappedTypeAsClause.ts, 18, 9))
>MappedModel : Symbol(MappedModel, Decl(genericMappedTypeAsClause.ts, 3, 2))
>T : Symbol(T, Decl(genericMappedTypeAsClause.ts, 12, 12))
>undefined : Symbol(undefined)
}

View File

@ -0,0 +1,67 @@
=== tests/cases/compiler/genericMappedTypeAsClause.ts ===
type Model = {
>Model : { a: string; b: number; }
a: string;
>a : string
b: number;
>b : number
};
type MappedModel<Suffix extends string> = {
>MappedModel : MappedModel<Suffix>
[K in keyof Model as `${K}${Suffix}`]: Model[K];
};
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
>foo1 : MappedModel<"Foo">
>{ aFoo: 'test', bFoo: 42 } : { aFoo: string; bFoo: number; }
>aFoo : string
>'test' : "test"
>bFoo : number
>42 : 42
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
>foo2 : MappedModel<"Foo">
>{ bFoo: 'bar' } : { bFoo: string; }
>bFoo : string
>'bar' : "bar"
function f1<T extends string>() {
>f1 : <T extends string>() => void
const x1: MappedModel<T> = 42; // Error
>x1 : MappedModel<T>
>42 : 42
const x2: MappedModel<T> = 'test'; // Error
>x2 : MappedModel<T>
>'test' : "test"
const x3: MappedModel<T> = [1, 2, 3]; // Error
>x3 : MappedModel<T>
>[1, 2, 3] : number[]
>1 : 1
>2 : 2
>3 : 3
const x4: MappedModel<T> = false; // Error
>x4 : MappedModel<T>
>false : false
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
>x5 : MappedModel<T>
>{ a: 'bar', b: 42 } : { a: string; b: number; }
>a : string
>'bar' : "bar"
>b : number
>42 : 42
const x6: MappedModel<T> = undefined; // Error
>x6 : MappedModel<T>
>undefined : undefined
}

View File

@ -9,7 +9,7 @@ type TU3 = Uppercase<string>; // Uppercase<string>
>TU3 : Uppercase<string>
type TU4 = Uppercase<any>; // Uppercase<`${any}`>
>TU4 : Uppercase<`${any}`>
>TU4 : Uppercase<any>
type TU5 = Uppercase<never>; // never
>TU5 : never
@ -27,7 +27,7 @@ type TL3 = Lowercase<string>; // Lowercase<string>
>TL3 : Lowercase<string>
type TL4 = Lowercase<any>; // Lowercase<`${any}`>
>TL4 : Lowercase<`${any}`>
>TL4 : Lowercase<any>
type TL5 = Lowercase<never>; // never
>TL5 : never
@ -45,7 +45,7 @@ type TC3 = Capitalize<string>; // Capitalize<string>
>TC3 : Capitalize<string>
type TC4 = Capitalize<any>; // Capitalize<`${any}`>
>TC4 : Capitalize<`${any}`>
>TC4 : Capitalize<any>
type TC5 = Capitalize<never>; // never
>TC5 : never
@ -63,7 +63,7 @@ type TN3 = Uncapitalize<string>; // Uncapitalize<string>
>TN3 : Uncapitalize<string>
type TN4 = Uncapitalize<any>; // Uncapitalize<`${any}`>
>TN4 : Uncapitalize<`${any}`>
>TN4 : Uncapitalize<any>
type TN5 = Uncapitalize<never>; // never
>TN5 : never
@ -72,13 +72,13 @@ type TN6 = Uncapitalize<42>; // Error
>TN6 : 42
type TX1<S extends string> = Uppercase<`aB${S}`>;
>TX1 : Uppercase<`aB${S}`>
>TX1 : `AB${Uppercase<S>}`
type TX2 = TX1<'xYz'>; // "ABXYZ"
>TX2 : "ABXYZ"
type TX3<S extends string> = Lowercase<`aB${S}`>;
>TX3 : Lowercase<`aB${S}`>
>TX3 : `ab${Lowercase<S>}`
type TX4 = TX3<'xYz'>; // "abxyz"
>TX4 : "abxyz"

View File

@ -61,7 +61,7 @@ type TD1 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b
>b : number
type TD2 = keyof TD1; // 'a1' | 'a2' | 'b1' | 'b2'
>TD2 : "a1" | "a2" | "b1" | "b2"
>TD2 : "a1" | "b1" | "a2" | "b2"
type TD3<U> = keyof DoubleProp<U>; // `${keyof U & string}1` | `${keyof U & string}2`
>TD3 : `${keyof U & string}1` | `${keyof U & string}2`

View File

@ -12,7 +12,7 @@ type T3<T extends string> = string & `${T}`; // `${T}
>T3 : `${T}`
type T4<T extends string> = string & `${Capitalize<`${T}`>}`; // `${Capitalize<T>}`
>T4 : `${Capitalize<`${T}`>}`
>T4 : `${Capitalize<T>}`
function f1(a: boolean[], x: `${number}`) {
>f1 : (a: boolean[], x: `${number}`) => void

View File

@ -1,6 +1,6 @@
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(7,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<Lowercase<string>>'.
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(15,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(16,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(15,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<Lowercase<`${number}`>>'.
tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(16,1): error TS2322: Type 'string' is not assignable to type 'Uppercase<Lowercase<`${number}`>>'.
==== tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts (3 errors) ====
@ -22,7 +22,7 @@ tests/cases/conformance/types/literal/stringLiteralsAssignedToStringMappings.ts(
// bad
y = "a";
~
!!! error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.
!!! error TS2322: Type 'string' is not assignable to type 'Uppercase<Lowercase<`${number}`>>'.
y = "A";
~
!!! error TS2322: Type 'string' is not assignable to type 'Uppercase<`${Lowercase<`${number}`>}`>'.
!!! error TS2322: Type 'string' is not assignable to type 'Uppercase<Lowercase<`${number}`>>'.

View File

@ -15,22 +15,22 @@ x = "a";
>"a" : "a"
declare var y: Uppercase<Lowercase<`${number}`>>;
>y : Uppercase<`${Lowercase<`${number}`>}`>
>y : Uppercase<Lowercase<`${number}`>>
// good
y = "1";
>y = "1" : "1"
>y : Uppercase<`${Lowercase<`${number}`>}`>
>y : Uppercase<Lowercase<`${number}`>>
>"1" : "1"
// bad
y = "a";
>y = "a" : "a"
>y : Uppercase<`${Lowercase<`${number}`>}`>
>y : Uppercase<Lowercase<`${number}`>>
>"a" : "a"
y = "A";
>y = "A" : "A"
>y : Uppercase<`${Lowercase<`${number}`>}`>
>y : Uppercase<Lowercase<`${number}`>>
>"A" : "A"

View File

@ -568,8 +568,8 @@ function ft1<T extends string>(t: T, u: Uppercase<T>, u1: Uppercase<`1.${T}.3`>,
>ft1 : <T extends string>(t: T, u: Uppercase<T>, u1: Uppercase<`1.${T}.3`>, u2: Uppercase<`1.${T}.4`>) => void
>t : T
>u : Uppercase<T>
>u1 : Uppercase<`1.${T}.3`>
>u2 : Uppercase<`1.${T}.4`>
>u1 : `1.${Uppercase<T>}.3`
>u2 : `1.${Uppercase<T>}.4`
spread(`1.${t}.3`, `1.${t}.4`);
>spread(`1.${t}.3`, `1.${t}.4`) : `1.${T}.3` | `1.${T}.4`
@ -588,9 +588,9 @@ function ft1<T extends string>(t: T, u: Uppercase<T>, u1: Uppercase<`1.${T}.3`>,
>u : Uppercase<T>
spread(u1, u2);
>spread(u1, u2) : Uppercase<`1.${T}.3`> | Uppercase<`1.${T}.4`>
>spread(u1, u2) : `1.${Uppercase<T>}.3` | `1.${Uppercase<T>}.4`
>spread : <P extends `${string}.${string}.${string}`>(...args: P[]) => P
>u1 : Uppercase<`1.${T}.3`>
>u2 : Uppercase<`1.${T}.4`>
>u1 : `1.${Uppercase<T>}.3`
>u2 : `1.${Uppercase<T>}.4`
}

View File

@ -0,0 +1,22 @@
// @strict: true
type Model = {
a: string;
b: number;
};
type MappedModel<Suffix extends string> = {
[K in keyof Model as `${K}${Suffix}`]: Model[K];
};
const foo1: MappedModel<'Foo'> = { aFoo: 'test', bFoo: 42 };
const foo2: MappedModel<'Foo'> = { bFoo: 'bar' }; // Error
function f1<T extends string>() {
const x1: MappedModel<T> = 42; // Error
const x2: MappedModel<T> = 'test'; // Error
const x3: MappedModel<T> = [1, 2, 3]; // Error
const x4: MappedModel<T> = false; // Error
const x5: MappedModel<T> = { a: 'bar', b: 42 }; // Error
const x6: MappedModel<T> = undefined; // Error
}