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
Properly defer resolution of mapped types with generic as
clauses
#51050
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 other generic types than the iteration type. | ||
// To determine this, we substitute the constraint type (that we now know isn't generic) for the iteration | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're ever mapping on an empty type, you will end up with interface Events {
// empty for now
}
type OnOffEvents<K extends "on" | "off"> = {
[P in keyof Events as `${K}${P}`]: (event: Events[P]) => void
}; If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think any special handling is needed here. |
||
// 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]; | ||
} | ||
Comment on lines
+15658
to
+15661
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this correct for if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The string mapping types are all constrained to |
||
} | ||
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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>'. | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will fix.