Skip to content
Draft
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
57 changes: 12 additions & 45 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8070,16 +8070,9 @@ func (c *Checker) checkIndexedAccessIndexType(t *Type, accessNode *ast.Node) *Ty
// Check if the index type is assignable to 'keyof T' for the object type.
objectType := t.AsIndexedAccessType().objectType
indexType := t.AsIndexedAccessType().indexType
// skip index type deferral on remapping mapped types
var objectIndexType *Type
if c.isGenericMappedType(objectType) && c.getMappedTypeNameTypeKind(objectType) == MappedTypeNameTypeKindRemapping {
objectIndexType = c.getIndexTypeForMappedType(objectType, IndexFlagsNone)
} else {
objectIndexType = c.getIndexTypeEx(objectType, IndexFlagsNone)
}
hasNumberIndexInfo := c.getIndexInfoOfType(objectType, c.numberType) != nil
if everyType(indexType, func(t *Type) bool {
return c.isTypeAssignableTo(t, objectIndexType) || hasNumberIndexInfo && c.isApplicableIndexType(t, c.numberType)
return c.isTypeAssignableTo(t, c.getIndexTypeEx(objectType, IndexFlagsNone)) || hasNumberIndexInfo && c.isApplicableIndexType(t, c.numberType)
}) {
if accessNode.Kind == ast.KindElementAccessExpression && ast.IsAssignmentTarget(accessNode) && objectType.objectFlags&ObjectFlagsMapped != 0 && getMappedTypeModifiers(objectType)&MappedTypeModifiersIncludeReadonly != 0 {
c.error(accessNode, diagnostics.Index_signature_in_type_0_only_permits_reading, c.TypeToString(objectType))
Expand Down Expand Up @@ -26063,46 +26056,11 @@ func (c *Checker) getSubstitutionIntersection(t *Type) *Type {
func (c *Checker) shouldDeferIndexType(t *Type, indexFlags IndexFlags) bool {
return t.flags&TypeFlagsInstantiableNonPrimitive != 0 ||
c.isGenericTupleType(t) ||
c.isGenericMappedType(t) && (!c.hasDistributiveNameType(t) || c.getMappedTypeNameTypeKind(t) == MappedTypeNameTypeKindRemapping) ||
c.isGenericMappedType(t) && c.getNameTypeFromMappedType(t) != nil ||
t.flags&TypeFlagsUnion != 0 && indexFlags&IndexFlagsNoReducibleCheck == 0 && c.isGenericReducibleType(t) ||
t.flags&TypeFlagsIntersection != 0 && c.maybeTypeOfKind(t, TypeFlagsInstantiable) && core.Some(t.Types(), c.IsEmptyAnonymousObjectType)
}

// Ordinarily we reduce a keyof M, where M is a mapped type { [P in K as N<P>]: X }, to simply N<K>. This however presumes
// that N distributes over union types, i.e. that N<A | B | C> is equivalent to N<A> | N<B> | N<C>. Specifically, we only
// want to perform the reduction when the name type of a mapped type is distributive with respect to the type variable
// introduced by the 'in' clause of the mapped type. Note that non-generic types are considered to be distributive because
// they're the same type regardless of what's being distributed over.
func (c *Checker) hasDistributiveNameType(mappedType *Type) bool {
typeVariable := c.getTypeParameterFromMappedType(mappedType)
var isDistributive func(*Type) bool
isDistributive = func(t *Type) bool {
switch {
case t.flags&(TypeFlagsAnyOrUnknown|TypeFlagsPrimitive|TypeFlagsNever|TypeFlagsTypeParameter|TypeFlagsObject|TypeFlagsNonPrimitive) != 0:
return true
case t.flags&TypeFlagsConditional != 0:
return t.AsConditionalType().root.isDistributive && t.AsConditionalType().checkType == typeVariable
case t.flags&TypeFlagsUnionOrIntersection != 0:
return core.Every(t.Types(), isDistributive)
case t.flags&TypeFlagsTemplateLiteral != 0:
return core.Every(t.AsTemplateLiteralType().types, isDistributive)
case t.flags&TypeFlagsIndexedAccess != 0:
return isDistributive(t.AsIndexedAccessType().objectType) && isDistributive(t.AsIndexedAccessType().indexType)
case t.flags&TypeFlagsSubstitution != 0:
return isDistributive(t.AsSubstitutionType().baseType) && isDistributive(t.AsSubstitutionType().constraint)
case t.flags&TypeFlagsStringMapping != 0:
return isDistributive(t.Target())
default:
return false
}
}
nameType := c.getNameTypeFromMappedType(mappedType)
if nameType == nil {
nameType = typeVariable
}
return isDistributive(nameType)
}

func (c *Checker) getMappedTypeNameTypeKind(t *Type) MappedTypeNameTypeKind {
nameType := c.getNameTypeFromMappedType(t)
if nameType == nil {
Expand Down Expand Up @@ -26155,7 +26113,7 @@ func (c *Checker) getIndexTypeForMappedType(t *Type, indexFlags IndexFlags) *Typ
// a circular definition. For this reason, we only eagerly manifest the keys if the constraint is non-generic.
if c.isGenericIndexType(constraintType) {
if c.isMappedTypeWithKeyofConstraintDeclaration(t) {
// We have a generic index and a homomorphic mapping (but a distributive key remapping) - we need to defer
// We have a generic index and a homomorphic mapping and a key remapping - we need to defer
// the whole `keyof whatever` for later since it's not safe to resolve the shape of modifier type.
return c.getIndexTypeForGenericType(t, indexFlags)
}
Expand Down Expand Up @@ -27117,6 +27075,8 @@ func (c *Checker) getSimplifiedType(t *Type, writing bool) *Type {
return c.getSimplifiedIndexedAccessType(t, writing)
case t.flags&TypeFlagsConditional != 0:
return c.getSimplifiedConditionalType(t, writing)
case t.flags&TypeFlagsIndex != 0:
return c.getSimplifiedIndexType(t)
}
return t
}
Expand Down Expand Up @@ -27232,6 +27192,13 @@ func (c *Checker) getSimplifiedConditionalType(t *Type, writing bool) *Type {
return t
}

func (c *Checker) getSimplifiedIndexType(t *Type) *Type {
if c.isGenericMappedType(t.AsIndexType().target) && c.getNameTypeFromMappedType(t.AsIndexType().target) != nil && !c.isMappedTypeWithKeyofConstraintDeclaration(t.AsIndexType().target) {
return c.getIndexTypeForMappedType(t.AsIndexType().target, IndexFlagsNone)
}
return t
}

// Invokes union simplification logic to determine if an intersection is considered empty as a union constituent
func (c *Checker) isIntersectionEmpty(type1 *Type, type2 *Type) bool {
return c.getUnionType([]*Type{c.intersectTypes(type1, type2), c.neverType}).flags&TypeFlagsNever != 0
Expand Down
142 changes: 142 additions & 0 deletions internal/checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,148 @@ foo.bar;`
}
}

// TestKeyRemappingKeyofResult2 tests that index types for generic mapped types with name types
// don't crash (regression test for microsoft/TypeScript#56239)
func TestKeyRemappingKeyofResult2(t *testing.T) {
t.Parallel()

content := `// https://github.com/microsoft/TypeScript/issues/56239

type Values<T> = T[keyof T];

type ProvidedActor = {
src: string;
logic: unknown;
};

interface StateMachineConfig<TActors extends ProvidedActor> {
invoke: {
src: TActors["src"];
};
}

declare function setup<TActors extends Record<string, unknown>>(_: {
actors: {
[K in keyof TActors]: TActors[K];
};
}): {
createMachine: (
config: StateMachineConfig<
Values<{
[K in keyof TActors as K & string]: {
src: K;
logic: TActors[K];
};
}>
>,
) => void;
};`

fs := vfstest.FromMap(map[string]string{
"/test.ts": content,
"/tsconfig.json": `{
"compilerOptions": {
"strict": true,
"noEmit": true
},
"files": ["test.ts"]
}`,
}, false)
fs = bundled.WrapFS(fs)

cd := "/"
host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil)

parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, host, nil)
assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")

p := compiler.NewProgram(compiler.ProgramOptions{
Config: parsed,
Host: host,
})
p.BindSourceFiles()
c, done := p.GetTypeChecker(t.Context())
defer done()

// The test passes if we can get a type checker without crashing
assert.Assert(t, c != nil)
}

// TestMappedTypeAsClauseRecursiveNoCrash tests that recursive mapped types with as clauses
// don't crash when computing keyof (regression test for microsoft/TypeScript#60476)
func TestMappedTypeAsClauseRecursiveNoCrash(t *testing.T) {
t.Parallel()

content := `// https://github.com/microsoft/TypeScript/issues/60476

export type FlattenType<Source extends object, Target> = {
[Key in keyof Source as Key extends string
? Source[Key] extends object
? ` + "`${Key}.${keyof FlattenType<Source[Key], Target> & string}`" + `
: Key
: never]-?: Target;
};

type FieldSelect = {
table: string;
field: string;
};

type Address = {
postCode: string;
description: string;
address: string;
};

type User = {
id: number;
name: string;
address: Address;
};

type FlattenedUser = FlattenType<User, FieldSelect>;
type FlattenedUserKeys = keyof FlattenType<User, FieldSelect>;

export type FlattenTypeKeys<Source extends object, Target> = keyof {
[Key in keyof Source as Key extends string
? Source[Key] extends object
? ` + "`${Key}.${keyof FlattenType<Source[Key], Target> & string}`" + `
: Key
: never]-?: Target;
};

type FlattenedUserKeys2 = FlattenTypeKeys<User, FieldSelect>;`

fs := vfstest.FromMap(map[string]string{
"/test.ts": content,
"/tsconfig.json": `{
"compilerOptions": {
"strict": true,
"noEmit": true
},
"files": ["test.ts"]
}`,
}, false)
fs = bundled.WrapFS(fs)

cd := "/"
host := compiler.NewCompilerHost(cd, fs, bundled.LibPath(), nil, nil)

parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, host, nil)
assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")

p := compiler.NewProgram(compiler.ProgramOptions{
Config: parsed,
Host: host,
})
p.BindSourceFiles()
c, done := p.GetTypeChecker(t.Context())
defer done()

// The test passes if we can get a type checker without crashing
assert.Assert(t, c != nil)
}

func BenchmarkNewChecker(b *testing.B) {
repo.SkipIfNoTypeScriptSubmodule(b)
fs := osvfs.FS()
Expand Down
2 changes: 1 addition & 1 deletion internal/checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ const (
TypeFlagsInstantiable = TypeFlagsInstantiableNonPrimitive | TypeFlagsInstantiablePrimitive
TypeFlagsStructuredOrInstantiable = TypeFlagsStructuredType | TypeFlagsInstantiable
TypeFlagsObjectFlagsType = TypeFlagsAny | TypeFlagsNullable | TypeFlagsNever | TypeFlagsObject | TypeFlagsUnion | TypeFlagsIntersection
TypeFlagsSimplifiable = TypeFlagsIndexedAccess | TypeFlagsConditional
TypeFlagsSimplifiable = TypeFlagsIndexedAccess | TypeFlagsConditional | TypeFlagsIndex
TypeFlagsSingleton = TypeFlagsAny | TypeFlagsUnknown | TypeFlagsString | TypeFlagsNumber | TypeFlagsBoolean | TypeFlagsBigInt | TypeFlagsESSymbol | TypeFlagsVoid | TypeFlagsUndefined | TypeFlagsNull | TypeFlagsNever | TypeFlagsNonPrimitive
// 'TypeFlagsNarrowable' types are types where narrowing actually narrows.
// This *should* be every type other than null, undefined, void, and never
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ mappedTypeConstraints2.ts(16,11): error TS2322: Type 'Mapped3<K>[Uppercase<K>]'
Type 'Mapped3<K>[Uppercase<string>]' is not assignable to type '{ a: K; }'.
Type 'Mapped3<K>[string]' is not assignable to type '{ a: K; }'.
mappedTypeConstraints2.ts(42,7): error TS2322: Type 'Mapped6<K>[keyof Mapped6<K>]' is not assignable to type '`_${string}`'.
Type 'Mapped6<K>[string] | Mapped6<K>[number] | Mapped6<K>[symbol]' is not assignable to type '`_${string}`'.
Type 'Mapped6<K>[string]' is not assignable to type '`_${string}`'.
Type 'Mapped6<K>[`_${K}`]' is not assignable to type '`_${string}`'.
Type 'Mapped6<K>[`_${string}`]' is not assignable to type '`_${string}`'.
mappedTypeConstraints2.ts(51,57): error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.
mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K>[`_${K}`]' is not assignable to type 'true'.
Expand Down Expand Up @@ -64,8 +64,8 @@ mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K
let s: `_${string}` = obj[key]; // Error
~
!!! error TS2322: Type 'Mapped6<K>[keyof Mapped6<K>]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[string] | Mapped6<K>[number] | Mapped6<K>[symbol]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[string]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[`_${K}`]' is not assignable to type '`_${string}`'.
!!! error TS2322: Type 'Mapped6<K>[`_${string}`]' is not assignable to type '`_${string}`'.
}

// Repro from #47794
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--- old.mappedTypeConstraints2.errors.txt
+++ new.mappedTypeConstraints2.errors.txt
@@= skipped -3, +3 lines =@@
Type 'Mapped3<K>[Uppercase<string>]' is not assignable to type '{ a: K; }'.
Type 'Mapped3<K>[string]' is not assignable to type '{ a: K; }'.
mappedTypeConstraints2.ts(42,7): error TS2322: Type 'Mapped6<K>[keyof Mapped6<K>]' is not assignable to type '`_${string}`'.
- Type 'Mapped6<K>[string] | Mapped6<K>[number] | Mapped6<K>[symbol]' is not assignable to type '`_${string}`'.
- Type 'Mapped6<K>[string]' is not assignable to type '`_${string}`'.
+ Type 'Mapped6<K>[`_${K}`]' is not assignable to type '`_${string}`'.
+ Type 'Mapped6<K>[`_${string}`]' is not assignable to type '`_${string}`'.
mappedTypeConstraints2.ts(51,57): error TS2322: Type 'Foo<T>[`get${T}`]' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'Foo<T>[`get${T}`]'.
mappedTypeConstraints2.ts(82,9): error TS2322: Type 'ObjectWithUnderscoredKeys<K>[`_${K}`]' is not assignable to type 'true'.
@@= skipped -60, +60 lines =@@
let s: `_${string}` = obj[key]; // Error
~
!!! error TS2322: Type 'Mapped6<K>[keyof Mapped6<K>]' is not assignable to type '`_${string}`'.
-!!! error TS2322: Type 'Mapped6<K>[string] | Mapped6<K>[number] | Mapped6<K>[symbol]' is not assignable to type '`_${string}`'.
-!!! error TS2322: Type 'Mapped6<K>[string]' is not assignable to type '`_${string}`'.
+!!! error TS2322: Type 'Mapped6<K>[`_${K}`]' is not assignable to type '`_${string}`'.
+!!! error TS2322: Type 'Mapped6<K>[`_${string}`]' is not assignable to type '`_${string}`'.
}

// Repro from #47794
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ type Mapped5<K extends string> = {
};

function f5<K extends string>(obj: Mapped5<K>, key: keyof Mapped5<K>) {
>f5 : <K extends string>(obj: Mapped5<K>, key: K extends `_${string}` ? K : never) => void
>f5 : <K extends string>(obj: Mapped5<K>, key: keyof Mapped5<K>) => void
>obj : Mapped5<K>
>key : K extends `_${string}` ? K : never
>key : keyof Mapped5<K>

let s: `_${string}` = obj[key];
>s : `_${string}`
>obj[key] : Mapped5<K>[K extends `_${string}` ? K : never]
>obj[key] : Mapped5<K>[keyof Mapped5<K>]
>obj : Mapped5<K>
>key : K extends `_${string}` ? K : never
>key : keyof Mapped5<K>
}

// repro from #53066#issuecomment-1913384757
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@
>obj : Mapped4<K>
>key : K

@@= skipped -18, +18 lines =@@
};

@@= skipped -20, +20 lines =@@
function f5<K extends string>(obj: Mapped5<K>, key: keyof Mapped5<K>) {
->f5 : <K extends string>(obj: Mapped5<K>, key: keyof Mapped5<K>) => void
+>f5 : <K extends string>(obj: Mapped5<K>, key: K extends `_${string}` ? K : never) => void
>f5 : <K extends string>(obj: Mapped5<K>, key: keyof Mapped5<K>) => void
>obj : Mapped5<K>
->key : K extends `_${string}` ? K : never
+>key : keyof Mapped5<K>

let s: `_${string}` = obj[key];
>s : `_${string}`
->obj[key] : Mapped5<K>[K extends `_${string}` ? K : never]
+>obj[key] : Mapped5<K>[keyof Mapped5<K>]
>obj : Mapped5<K>
>key : K extends `_${string}` ? K : never
->key : K extends `_${string}` ? K : never
+>key : keyof Mapped5<K>
}

@@= skipped -74, +74 lines =@@
// repro from #53066#issuecomment-1913384757
@@= skipped -72, +72 lines =@@
>key : string
>val : any
>Object.entries(obj) : [string, any][]
Expand Down