{"record":{"id":"880a7b6c11a116c7","repo":"microsoft/aspire","slug":"resource-types-typeid-all-map-to-the-generated-typescript","errorCode":null,"errorMessage":"Resource types '{typeId}' all map to the generated TypeScript name '{group.Key}', but they are not a concrete type and its interfaces.","messagePattern":"Resource types '(.+?)' all map to the generated TypeScript name '(.+?)', but they are not a concrete type and its interfaces\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs","lineNumber":2846,"sourceCode":"            .ThenBy(builder => builder.BuilderClassName)\n            .GroupBy(builder => builder.BuilderClassName, StringComparer.Ordinal)\n            .Select(group =>\n            {\n                var candidates = group\n                    .OrderBy(builder => builder.IsInterface)\n                    .ThenBy(builder => builder.TypeId, StringComparer.Ordinal)\n                    .ToList();\n                var retainedBuilder = candidates[0];\n                var unrelatedBuilder = candidates\n                    .Skip(1)\n                    .FirstOrDefault(candidate => !IsBuilderAlias(retainedBuilder, candidate));\n\n                if (unrelatedBuilder is not null)\n                {\n                    var collidingTypeIds = candidates\n                        .Select(candidate => candidate.TypeId)\n                        .Order(StringComparer.Ordinal);\n                    throw new InvalidOperationException(\n                        $\"Resource types {string.Join(\", \", collidingTypeIds.Select(typeId => $\"'{typeId}'\"))} \" +\n                        $\"all map to the generated TypeScript name '{group.Key}', but they are not a concrete type and its interfaces.\");\n                }\n\n                return retainedBuilder;\n            })\n            .ToList();\n    }\n\n    private static void SortOptionsInterfaceCollisionsByCapabilityIdentity(List<AtsCapabilityInfo> capabilities)\n    {\n        // Reorder only colliding option-interface slots. Sorting every capability would rewrite\n        // long-established source order for methods unrelated to the collision.\n        var collisionGroups = capabilities\n            .Select((capability, index) => (Capability: capability, Index: index))\n            .Where(entry =>\n            {\n                var (_, optionalParameters) = SeparateParameters(entry.Capability.Parameters);","sourceCodeStart":2828,"sourceCodeEnd":2864,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs#L2828-L2864","documentation":"Aspire's TypeScript API generator collapses all resource types that map to the same generated TypeScript name into a single declaration. It allows a concrete type plus its interfaces to share a name, but when two or more type IDs with unrelated type relationships collide on the same generated name, the projector cannot pick a single valid representation and throws this InvalidOperationException at generation time.","triggerScenarios":"Defining two or more custom resource types (different TypeIds) whose generated TypeScript names normalize to the same identifier — e.g. types named 'MyRedis' and 'my-redis' or casing/prefix collisions — and building a project that runs the TypeScript code generation.","commonSituations":"Renaming a resource type so it now collides with an existing one; adding a second custom resource that differs only by case or separators; copying a resource definition from a sample without changing the generated name.","solutions":["Rename one of the colliding resource types so each TypeId produces a distinct TypeScript name","If one type is genuinely an interface of the other, verify the type hierarchy (implements/inherits) is declared so the projector can treat them as concrete + interfaces","Check the error message for the exact colliding type IDs and adjust the least-used one","Ensure no two custom resource builders register the same generated name via different type IDs"],"exampleFix":"// before\nvar builder = appBuilder.AddResource(new MyRedisResource(\"cache\"));   // typeId: MyRedis\nvar builder2 = appBuilder.AddResource(new MyRedisResource2(\"cache2\")); // typeId: my-redis -> same TS name\n// after\nvar builder = appBuilder.AddResource(new MyRedisResource(\"cache\"));    // typeId: MyRedis -> MyRedis\nvar builder2 = appBuilder.AddResource(new MyRedisClusterResource(\"cache2\")); // distinct TS name","handlingStrategy":"validation","validationCode":"var tsNames = new HashSet<string>(StringComparer.Ordinal);\nforeach (var resource in resourceTypes)\n{\n    if (!tsNames.Add(GenerateTypeScriptName(resource.TypeId)))\n        throw new InvalidOperationException($\"Type '{resource.TypeId}' collides on generated TypeScript name with an unrelated type.\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Give each custom resource type a unique, case-distinct TypeId","After renaming any resource type, regenerate TypeScript and check for collisions immediately","Keep generated names derived from one canonical casing convention","Model related types with a real inheritance/interface relationship so the projector can merge them legally"],"tags":["typescript","code-generation","naming-collision","invalid-state"],"backgroundTag":"schema-validation-failed","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}