Repository: microsoft/kiota (csharp)

Issue:
TypeScript better expression of enums
We're currently generating enums like that

```TypeScript
export enum BodyType {
    Text = "text",
    Html = "html",
}
```

Which transpiles down to 

```JavaScript
export var BodyType;
(function (BodyType) {
    BodyType["Text"] = "text";
    BodyType["Html"] = "html";
})(BodyType || (BodyType = {}));
```

We should probably instead consider generating this

```TypeScript
const BodyTypeConst = {
   Text: "text",
   Html: "html",
} as const;

export type BodyType = keyof typeof BodyTypeConst;
```

Not only this allows consumers to use the string literal and the symbol interchangeably, it results in this

```JavaScript
const BodyTypeConst = {
    Text: "text",
    Html: "html",
};
export {};
```

Pull request:
Fixes: https://github.com/microsoft/kiota/issues/2105

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df261baf48..bae412d5d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 
 - Fixed a bug in the vscode extension where the "Paste API Manifest" button would not be able to parse the manifest.
+- Enhance the way Enums are expressed in Typescript. [#2105](https://github.com/microsoft/kiota/issues/2105)
 
 ## [1.9.0] - 2023-12-07
 
diff --git a/src/Kiota.Builder/CodeDOM/CodeConstant.cs b/src/Kiota.Builder/CodeDOM/CodeConstant.cs
index 57a8f0e12e..e834b074e3 100644
--- a/src/Kiota.Builder/CodeDOM/CodeConstant.cs
+++ b/src/Kiota.Builder/CodeDOM/CodeConstant.cs
@@ -6,7 +6,7 @@ namespace Kiota.Builder.CodeDOM;
 
 public class CodeConstant : CodeTerminalWithKind<CodeConstantKind>
 {
-    public CodeInterface? OriginalInterface
+    public CodeElement? OriginalCodeElement
     {
         get;
         set;
@@ -20,11 +20,22 @@ public CodeInterface? OriginalInterface
         {
             Name = $"{source.Name.ToFirstCharacterLowerCase()}Mapper",
             Kind = CodeConstantKind.QueryParametersMapper,
-            OriginalInterface = source,
+            OriginalCodeElement = source,
+        };
+    }
+    public static CodeConstant? FromCodeEnum(CodeEnum source)
+    {
+        ArgumentNullException.ThrowIfNull(source);
+        return new CodeConstant
+        {
+            Name = $"{source.Name.ToFirstCharacterLowerCase()}Object",
+            Kind = CodeConstantKind.EnumObject,
+            OriginalCodeElement = source,
         };
     }
 }
 public enum CodeConstantKind
 {
     QueryParametersMapper,
+    EnumObject,
 }
diff --git a/src/Kiota.Builder/CodeDOM/CodeEnum.cs b/src/Kiota.Builder/CodeDOM/CodeEnum.cs
index 61c187fdc6..3a46dd0a40 100644
--- a/src/Kiota.Builder/CodeDOM/CodeEnum.cs
+++ b/src/Kiota.Builder/CodeDOM/CodeEnum.cs
@@ -1,4 +1,5 @@
-﻿using System.Collections.Concurrent;
+﻿using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -36,4 +37,8 @@ public DeprecationInformation? Deprecation
     {
         get; set;
     }
+    public CodeConstant? CodeEnumObject
+    {
+        get; set;
+    }
 }
diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
index 32ab9623e2..0646e6f161 100644
--- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
+++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
@@ -148,6 +148,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
                 generatedCode,
                 factoryNameCallbackFromType
             );
+
             AddStaticMethodsUsingsForRequestExecutor(
                 generatedCode,
                 factoryNameCallbackFromType
@@ -159,6 +160,7 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
                 ],
                 static s => s.ToCamelCase(UnderscoreArray));
             IntroducesInterfacesAndFunctions(generatedCode, factoryNameCallbackFromType);
+            GenerateEnumObjects(generatedCode);
             AliasUsingsWithSameSymbol(generatedCode);
             var modelsNamespace = generatedCode.FindOrAddNamespace(_configuration.ModelsNamespaceName); // ensuring we have a models namespace in case we don't have any reusable model
             GenerateReusableModelsCodeFiles(modelsNamespace);
@@ -168,6 +170,11 @@ public override Task Refine(CodeNamespace generatedCode, CancellationToken cance
             cancellationToken.ThrowIfCancellationRequested();
         }, cancellationToken);
     }
+    private static void GenerateEnumObjects(CodeElement currentElement)
+    {
+        AddEnumObject(currentElement);
+        AddEnumObjectUsings(currentElement);
+    }
     private const string FileNameForModels = "index";
     private static void GroupReusableModelsInSingleFile(CodeElement currentElement)
     {
@@ -182,7 +189,10 @@ private static void GroupReusableModelsInSingleFile(CodeElement currentElement)
             }
             if (codeNamespace.Enums.ToArray() is { Length: > 0 } enums)
             {
+                var enumObjects = enums.Select(static x => x.CodeEnumObject).OfType<CodeConstant>().ToArray();
+                targetFile.AddElements(enumObjects);
                 targetFile.AddElements(enums);
+                codeNamespace.RemoveChildElement(enumObjects);
                 codeNamespace.RemoveChildElement(enums);
             }
             RemoveSelfReferencingUsingForFile(targetFile, codeNamespace);
@@ -232,7 +242,6 @@ codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Factory &&
             return null;
         return codeNamespace.TryAddCodeFile(codeInterface.Name, [codeInterface, .. functions]);
     }
-
     private static void GenerateRequestBuilderCodeFile(CodeClass codeClass, CodeNamespace codeNamespace)
     {
         var executorMethods = codeClass.Methods
@@ -243,6 +252,11 @@ private static void GenerateRequestBuilderCodeFile(CodeClass codeClass, CodeName
             .Enums
             .ToArray();
 
+        var enumObjects = inlineEnums
+            .Select(static x => x.CodeEnumObject)
+            .OfType<CodeConstant>()
+            .ToArray();
+
         var queryParameterInterfaces = executorMethods
             .SelectMany(static x => x.Parameters)
             .Where(static x => x.IsOfKind(CodeParameterKind.RequestConfiguration))
@@ -272,6 +286,7 @@ private static void GenerateRequestBuilderCodeFile(CodeClass codeClass, CodeName
                             .Union(queryParametersMapperConstants)
                             .Union(inlineRequestAndResponseBodyFiles.SelectMany(static x => x.GetChildElements(true)))
                             .Union(inlineEnums)
+                            .Union(enumObjects)
                             .Distinct()
                             .ToArray();
 
@@ -561,7 +576,6 @@ codeClass.Parent is CodeClass parentClass &&
             var props = codeClass.Properties.ToArray();
             codeInterface.AddProperty(props);
 
-
             if (CodeConstant.FromQueryParametersMapping(codeInterface) is CodeConstant constant)
                 targetNS.AddConstant(constant);
 
@@ -1015,6 +1029,36 @@ private static void SetUsingsOfPropertyInSerializationFunctions(string propertyS
         }
     }
 
+    protected static void AddEnumObject(CodeElement currentElement)
+    {
+        if (currentElement is CodeEnum codeEnum && CodeConstant.FromCodeEnum(codeEnum) is CodeConstant constant)
+        {
+            codeEnum.CodeEnumObject = constant;
+            var nameSpace = codeEnum.GetImmediateParentOfType<CodeNamespace>();
+            nameSpace.AddConstant(constant);
+        }
+        CrawlTree(currentElement, AddEnumObject);
+    }
+
+    protected static void AddEnumObjectUsings(CodeElement currentElement)
+    {
+        if (currentElement is CodeFunction codeFunction && codeFunction.OriginalLocalMethod.IsOfKind(CodeMethodKind.Deserializer, CodeMethodKind.Serializer))
+        {
+            foreach (var propertyEnum in codeFunction.OriginalMethodParentClass.Properties.Select(static x => x.Type).OfType<CodeType>().Select(static x => x.TypeDefinition).OfType<CodeEnum>())
+            {
+                codeFunction.AddUsing(new CodeUsing
+                {
+                    Name = propertyEnum.Name,
+                    Declaration = new CodeType
+                    {
+                        TypeDefinition = propertyEnum.CodeEnumObject
+                    }
+                });
+            }
+        }
+        CrawlTree(currentElement, AddEnumObjectUsings);
+    }
+
     private static void ProcessModelClassProperties(CodeClass modelClass, CodeInterface modelInterface, IEnumerable<CodeProperty> properties, Func<CodeClass, string> interfaceNamingCallback)
     {
         /*
diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeBlockEndWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeBlockEndWriter.cs
index ad1e6d07e9..fa9a2b31a4 100644
--- a/src/Kiota.Builder/Writers/TypeScript/CodeBlockEndWriter.cs
+++ b/src/Kiota.Builder/Writers/TypeScript/CodeBlockEndWriter.cs
@@ -11,7 +11,7 @@ public override void WriteCodeElement(BlockEnd codeElement, LanguageWriter write
     {
         ArgumentNullException.ThrowIfNull(codeElement);
         ArgumentNullException.ThrowIfNull(writer);
-        if (codeElement.Parent is CodeNamespace) return;
+        if (codeElement.Parent is CodeNamespace or CodeEnum) return;
         writer.CloseBlock();
         if (codeElement.Parent?.Parent is CodeNamespace)
             conventions.WriteAutoGeneratedEnd(writer);
diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
index 082ed5b53c..70e9b5fee2 100644
--- a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
+++ b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
@@ -2,6 +2,7 @@
 using System.Linq;
 using Kiota.Builder.CodeDOM;
 using Kiota.Builder.Extensions;
+using Kiota.Builder.Writers.Go;
 
 namespace Kiota.Builder.Writers.TypeScript;
 public class CodeConstantWriter : BaseElementWriter<CodeConstant, TypeScriptConventionService>
@@ -11,9 +12,22 @@ public override void WriteCodeElement(CodeConstant codeElement, LanguageWriter w
     {
         ArgumentNullException.ThrowIfNull(codeElement);
         ArgumentNullException.ThrowIfNull(writer);
-        if (codeElement.OriginalInterface is null) throw new InvalidOperationException("Original interface cannot be null");
+        if (codeElement.OriginalCodeElement is null) throw new InvalidOperationException("Original CodeElement cannot be null");
+        switch (codeElement.Kind)
+        {
+            case CodeConstantKind.QueryParametersMapper:
+                WriteQueryParametersMapperConstant(codeElement, writer);
+                break;
+            case CodeConstantKind.EnumObject:
+                WriteEnumObjectConstant(codeElement, writer);
+                break;
+        }
+    }
+    private static void WriteQueryParametersMapperConstant(CodeConstant codeElement, LanguageWriter writer)
+    {
+        if (codeElement.OriginalCodeElement is not CodeInterface codeInterface) throw new InvalidOperationException("Original CodeElement cannot be null");
         writer.StartBlock($"const {codeElement.Name.ToFirstCharacterLowerCase()}: Record<string, string> = {{");
-        foreach (var property in codeElement.OriginalInterface
+        foreach (var property in codeInterface
                                                 .Properties
                                                 .OfKind(CodePropertyKind.QueryParameter)
                                                 .Where(static x => !string.IsNullOrEmpty(x.SerializationName))
@@ -23,4 +37,18 @@ public override void WriteCodeElement(CodeConstant codeElement, LanguageWriter w
         }
         writer.CloseBlock("};");
     }
+    private void WriteEnumObjectConstant(CodeConstant codeElement, LanguageWriter writer)
+    {
+        if (codeElement.OriginalCodeElement is not CodeEnum codeEnum) throw new InvalidOperationException("Original CodeElement cannot be null");
+        if (!codeEnum.Options.Any())
+            return;
+        conventions.WriteLongDescription(codeEnum, writer);
+        writer.StartBlock($"export const {codeElement.Name.ToFirstCharacterUpperCase()} = {{");
+        codeEnum.Options.ToList().ForEach(x =>
+        {
+            conventions.WriteShortDescription(x.Documentation.Description, writer);
+            writer.WriteLine($"{x.Name.ToFirstCharacterUpperCase()}: \"{x.WireName}\",");
+        });
+        writer.CloseBlock("}  as const;");
+    }
 }
diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeEnumWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeEnumWriter.cs
index 80eec59131..6810b9be44 100644
--- a/src/Kiota.Builder/Writers/TypeScript/CodeEnumWriter.cs
+++ b/src/Kiota.Builder/Writers/TypeScript/CodeEnumWriter.cs
@@ -12,16 +12,10 @@ public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter write
     {
         ArgumentNullException.ThrowIfNull(codeElement);
         ArgumentNullException.ThrowIfNull(writer);
+        ArgumentNullException.ThrowIfNull(codeElement.CodeEnumObject);
         if (!codeElement.Options.Any())
             return;
-
-        conventions.WriteLongDescription(codeElement, writer);
-        writer.WriteLine($"export enum {codeElement.Name.ToFirstCharacterUpperCase()} {{");
-        writer.IncreaseIndent();
-        codeElement.Options.ToList().ForEach(x =>
-        {
-            conventions.WriteShortDescription(x.Documentation.Description, writer);
-            writer.WriteLine($"{x.Name.ToFirstCharacterUpperCase()} = \"{x.WireName}\",");
-        });
+        var enumObjectName = codeElement.CodeEnumObject.Name.ToFirstCharacterUpperCase();
+        writer.WriteLine($"export type {codeElement.Name.ToFirstCharacterUpperCase()} = (typeof {enumObjectName})[keyof typeof {enumObjectName}];");
     }
 }
diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
index 3e0c3bfb00..333df72eb2 100644
--- a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
+++ b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
@@ -223,8 +223,8 @@ private string GetDeserializationMethodName(CodeTypeBase propType, CodeFunction
         var propertyType = conventions.GetTypeString(propType, codeFunction, false);
         if (!string.IsNullOrEmpty(propertyType) && propType is CodeType currentType)
         {
-            if (currentType.TypeDefinition is CodeEnum currentEnum)
-                return $"{(currentEnum.Flags || isCollection ? "getCollectionOfEnumValues" : "getEnumValue")}<{currentEnum.Name.ToFirstCharacterUpperCase()}>({propertyType.ToFirstCharacterUpperCase()})";
+            if (currentType.TypeDefinition is CodeEnum currentEnum && currentEnum.CodeEnumObject is not null)
+                return $"{(currentEnum.Flags || isCollection ? "getCollectionOfEnumValues" : "getEnumValue")}<{currentEnum.Name.ToFirstCharacterUpperCase()}>({currentEnum.CodeEnumObject.Name.ToFirstCharacterUpperCase()})";
             else if (conventions.StreamTypeName.Equals(propertyType, StringComparison.OrdinalIgnoreCase))
                 return "getByteArrayValue";
             else if (isCollection)


Test patch:
diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs
new file mode 100644
index 0000000000..c61bcc422f
--- /dev/null
+++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs
@@ -0,0 +1,104 @@
+﻿using System;
+using System.IO;
+using System.Linq;
+using Kiota.Builder.CodeDOM;
+using Kiota.Builder.Writers;
+using Kiota.Builder.Writers.Go;
+using Kiota.Builder.Writers.TypeScript;
+
+using Xunit;
+
+namespace Kiota.Builder.Tests.Writers.TypeScript;
+public sealed class CodeConstantWriterTests : IDisposable
+{
+    private const string DefaultPath = "./";
+    private const string DefaultName = "name";
+    private readonly StringWriter tw;
+    private readonly LanguageWriter writer;
+    private readonly CodeEnum currentEnum;
+    private const string EnumName = "someEnum";
+    private readonly CodeConstantWriter codeConstantWriter;
+
+    public CodeConstantWriterTests()
+    {
+        writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName);
+        codeConstantWriter = new CodeConstantWriter(new());
+        tw = new StringWriter();
+        writer.SetTextWriter(tw);
+        var root = CodeNamespace.InitRootNamespace();
+        currentEnum = root.AddEnum(new CodeEnum
+        {
+            Name = EnumName,
+        }).First();
+        if (CodeConstant.FromCodeEnum(currentEnum) is CodeConstant constant)
+        {
+            currentEnum.CodeEnumObject = constant;
+            root.AddConstant(constant);
+        }
+    }
+    public void Dispose()
+    {
+        tw?.Dispose();
+        GC.SuppressFinalize(this);
+    }
+
+    [Fact]
+    public void WriteCodeElement_ThrowsException_WhenCodeElementIsNull()
+    {
+        Assert.Throws<ArgumentNullException>(() => codeConstantWriter.WriteCodeElement(null, writer));
+    }
+
+    [Fact]
+    public void WriteCodeElement_ThrowsException_WhenWriterIsNull()
+    {
+        var codeElement = new CodeConstant();
+        Assert.Throws<ArgumentNullException>(() => codeConstantWriter.WriteCodeElement(codeElement, null));
+    }
+
+    [Fact]
+    public void WriteCodeElement_ThrowsException_WhenOriginalCodeElementIsNull()
+    {
+        var codeElement = new CodeConstant();
+        Assert.Throws<InvalidOperationException>(() => codeConstantWriter.WriteCodeElement(codeElement, writer));
+    }
+
+    [Fact]
+    public void WritesEnumOptionDescription()
+    {
+        var option = new CodeEnumOption
+        {
+            Documentation = new()
+            {
+                Description = "Some option description",
+            },
+            Name = "option1",
+        };
+        currentEnum.AddOption(option);
+        codeConstantWriter.WriteCodeElement(currentEnum.CodeEnumObject, writer);
+        var result = tw.ToString();
+        Assert.Contains($"/** {option.Documentation.Description} */", result);
+        AssertExtensions.CurlyBracesAreClosed(result, 0);
+    }
+
+    [Fact]
+    public void WritesEnum()
+    {
+        const string optionName = "option1";
+        currentEnum.AddOption(new CodeEnumOption { Name = optionName });
+        codeConstantWriter.WriteCodeElement(currentEnum.CodeEnumObject, writer);
+        var result = tw.ToString();
+        Assert.Contains("export const SomeEnumObject = {", result);
+        Assert.Contains("Option1: \"option1\"", result);
+        Assert.Contains("as const;", result);
+        Assert.Contains(optionName, result);
+        AssertExtensions.CurlyBracesAreClosed(result, 0);
+    }
+
+    [Fact]
+    public void DoesntWriteAnythingOnNoOption()
+    {
+        codeConstantWriter.WriteCodeElement(currentEnum.CodeEnumObject, writer);
+        var result = tw.ToString();
+        Assert.Empty(result);
+    }
+}
diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeEnumWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeEnumWriterTests.cs
index 0ec67d3bd1..dae79cf062 100644
--- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeEnumWriterTests.cs
+++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeEnumWriterTests.cs
@@ -3,8 +3,9 @@
 using System.Linq;
 
 using Kiota.Builder.CodeDOM;
+using Kiota.Builder.Extensions;
 using Kiota.Builder.Writers;
-
+using Kiota.Builder.Writers.TypeScript;
 using Xunit;
 
 namespace Kiota.Builder.Tests.Writers.TypeScript;
@@ -16,9 +17,11 @@ public sealed class CodeEnumWriterTests : IDisposable
     private readonly LanguageWriter writer;
     private readonly CodeEnum currentEnum;
     private const string EnumName = "someEnum";
+    private readonly CodeEnumWriter codeEnumWriter;
     public CodeEnumWriterTests()
     {
         writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName);
+        codeEnumWriter = new CodeEnumWriter(new());
         tw = new StringWriter();
         writer.SetTextWriter(tw);
         var root = CodeNamespace.InitRootNamespace();
@@ -26,6 +29,11 @@ public CodeEnumWriterTests()
         {
             Name = EnumName,
         }).First();
+        if (CodeConstant.FromCodeEnum(currentEnum) is CodeConstant constant)
+        {
+            currentEnum.CodeEnumObject = constant;
+            root.AddConstant(constant);
+        }
     }
     public void Dispose()
     {
@@ -33,38 +41,33 @@ public void Dispose()
         GC.SuppressFinalize(this);
     }
     [Fact]
-    public void WritesEnum()
+    public void WriteCodeElement_ThrowsException_WhenCodeElementIsNull()
     {
-        const string optionName = "option1";
-        currentEnum.AddOption(new CodeEnumOption { Name = optionName });
-        writer.Write(currentEnum);
-        var result = tw.ToString();
-        Assert.Contains("export enum", result);
-        Assert.Contains(optionName, result);
-        AssertExtensions.CurlyBracesAreClosed(result, 1);
+        Assert.Throws<ArgumentNullException>(() => codeEnumWriter.WriteCodeElement(null, writer));
     }
     [Fact]
-    public void DoesntWriteAnythingOnNoOption()
+    public void WriteCodeElement_ThrowsException_WhenWriterIsNull()
     {
-        writer.Write(currentEnum);
+        var codeElement = new CodeEnum();
+        Assert.Throws<ArgumentNullException>(() => codeEnumWriter.WriteCodeElement(codeElement, null));
+    }
+    [Fact]
+    public void WritesEnum()
+    {
+        const string optionName = "option1";
+        currentEnum.AddOption(new CodeEnumOption { Name = optionName });
+        codeEnumWriter.WriteCodeElement(currentEnum, writer);
         var result = tw.ToString();
-        Assert.Empty(result);
+        Assert.Contains("export type SomeEnum = (", result);
+        Assert.Contains("typeof", result);
+        Assert.Contains(currentEnum.Name.ToFirstCharacterUpperCase(), result);
+        AssertExtensions.CurlyBracesAreClosed(result, 0);
     }
     [Fact]
-    public void WritesEnumOptionDescription()
+    public void DoesntWriteEnumWithEmptyOptions()
     {
-        var option = new CodeEnumOption
-        {
-            Documentation = new()
-            {
-                Description = "Some option description",
-            },
-            Name = "option1",
-        };
-        currentEnum.AddOption(option);
-        writer.Write(currentEnum);
+        codeEnumWriter.WriteCodeElement(currentEnum, writer);
         var result = tw.ToString();
-        Assert.Contains($"/** {option.Documentation.Description} */", result);
-        AssertExtensions.CurlyBracesAreClosed(result, 1);
+        Assert.Equal("", result);
     }
 }