Generates TypeScript-like utility types for C#.
Utility types are generated types based on one or more input types. Slap the [UtilityType(selector)]
attribute on a
partial
type and the generator will generate a partial type with the same name and type (e.g., class, record, struct)
as the type with the attribute (yes, that can be different from the input type(s)!), but with the specified selector(s) applied.
For more information about utility types and how to use them, check out the TypeScript docs.
Important Note: This only generates auto-properties, no matter whether the input type's properties are auto-properties or not. This can be handy in and of itself, but computed properties are out of scope for this project.
Another important note: This is a source generator, so it only works w/ .NET 5.0+. However, I'm opinionated about using the latest stable C# SDK, so YMMV if you are running something ancient (like C# 7.3). You should really be setting
<LangVersion>latest</LangVersion>
in your projects (yes, that works with older TargetFrameworks).
- Add the UtilityTypeGenerator NuGet package to your project:
<PackageReference Include="UtilityTypeGenerator" Version="0.0.9" PrivateAssets="all" IncludeAssets="build; analyzers" />
- Add the
[UtilityType("selector")]
attribute to apartial
type, replacing"selector"
with the selector(s) of your choice.
- The generated type will be a
partial
type with the same name, type, and accessibility as the type with the[UtilityType]
attribute. - Each property will have the same accessibility as the property in the input type.
- Comments (leading trivia) from the first matching property in the input type will be copied to the generated property.
- Initializer statements for properties will be stripped. If the property type is not nullable, the
= default!;
initializer will be added.
If you need to customize the generated type, you can simply provide whatever you need in the partial type. Make sure to exclude any properties that you don't want to be generated if they would conflict!
A selector is a string that specifies a verb (e.g., Pick
), one or more types or nested selectors, and (for some verbs) property names.
Verb | Syntax | Description |
---|---|---|
Import |
Import<T> |
Imports all of the properties from T (a type or selector). |
Intersection |
Intersection<T1, T2 [, T3] [...]> or Intersect<T1, T2 [, T3] [...]> |
Creates a type with the intersection of properties from T1 and T2 , etc. (types or selectors). Duplicate properties are okay, but the type of the property must be the same in both types. |
NotNull |
NotNull<T> |
Creates a type with all properties from T (a type or selector) transformed to non-nullable. |
Nullable |
Nullable<T> |
Creates a type with all properties from T (a type or selector) transformed to nullable. |
Omit |
`Omit<T, Property1 [ | Property2] [...]>or Omit<T, Property1 [, Property2] [...]>` |
Optional * |
Optional<T> |
Creates a type with all properties from T (a type or selector) stripped of the required keyword. * Optional<T> behaves differently than it does in TypeScript! See below for details. |
Pick |
`Pick<T, Property1 [ | Property2] [...]>or Pick<T, Property1 [, Property2] [...]>` |
Required |
Required<T> |
Creates a type with all properties from T (a type or selector) marked as required .Requires C# 11+ (or PolySharp!) |
Union |
Union<T1, T2 [, T3] [...]> |
Creates a type with the union of properties from T1 and T2 , etc. (types or selectors). Duplicate properties are okay, but the type of the property must be the same in both types. At least 2 types must be present in the selector. |
namespace MyNamespace;
using MyNamespace.InternalData;
public class Person
{
/// <summary>The unique identifier for the person.</summary>
public Guid Id { get; set; }
/// <summary>The name of the person.</summary>
public string? Name { get; set; }
/// <summary>The date of birth of the person.</summary>
public DateTimeOffset? BirthDate { get; set; }
/// <summary>Internal data object for that person.</summary>
internal PersonData Data { get; set; }
}
[UtilityType("Import<Person>")]
internal partial class Foo
{
public required string SomeOtherProperty { get; }
}
// generates:
internal partial class Foo
{
/// <summary>The unique identifier for the person.</summary>
public Guid Id { get; set; }
/// <summary>The name of the person.</summary>
public string? Name { get; set; }
/// <summary>The date of birth of the person.</summary>
public DateTimeOffset? BirthDate { get; set; }
/// <summary>Internal data object for that person.</summary>
internal global::MyNamespace.InternalData.PersonData Data { get; set; }
}
// since this is a partial class, SomeOtherProperty is also defined (but not in the generated source).
public class Person
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTimeOffset? BirthDate { get; set; }
}
[UtilityType("Pick<Person, Name>")]
public partial class OnlyName;
// generates:
public partial class OnlyName
{
public string? Name { get; set; }
}
public class Person
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTimeOffset? BirthDate { get; set; }
}
[UtilityType("Omit<Person, Name>")]
public partial class OmitName;
// generates:
public partial class OmitName
{
public Guid Id { get; set; }
public DateTimeOffset? BirthDate { get; set; }
}
public class Person
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTimeOffset? BirthDate { get; set; }
}
[UtilityType("Required<Person>")]
public partial class PersonRequired;
// generates:
public partial class PersonRequired
{
public Guid Id { get; set; } = default!;
public string Name { get; set; } = default!;
public DateTimeOffset BirthDate { get; set; } = default!;
}
IMPORTANT: This is different from TypeScript, where
Optional<T>
allowsundefined
values for the properties.
TIP: This should be combined with
Nullable<T>
to avoid NullReferenceExceptions for any reference type properties. Composition withPick<T>
andOmit<T>
can also be helpful.
public class Person
{
public required Guid Id { get; }
public required string? Name { get; }
public DateTimeOffset? BirthDate { get; set; }
}
[UtilityType("Optional<Person>")]
public partial class PersonOptional;
// generates:
public partial class PersonOptional
{
public Guid Id { get; set; } = default!;
public string Name { get; set; } = default!;
public DateTimeOffset? BirthDate { get; set; }
}
public class Person
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = "";
public DateTimeOffset BirthDate { get; set; } = DateTimeOffset.MinValue;
}
[UtilityType("Nullable<Person>")]
public partial class PersonWithNullableProperties;
// generates (note the default values are stripped!):
public partial class PersonWithNullableProperties
{
public Guid? Id { get; set; }
public string? Name { get; set; }
public DateTimeOffset? BirthDate { get; set; }
}
TIP: If you are trying to Union just one type, use
Import<T>
instead.
Syntax: Union<T1, T2 [, T3] [...]>
public class Person
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTimeOffset? BirthDate { get; set; }
}
public class User
{
public required Guid Id { get; set; }
public required string? UserName { get; set; }
}
[UtilityType("Union<Person, User>")]
public partial class PersonAndUser;
// generates:
public partial class PersonAndUser
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTimeOffset? BirthDate { get; set; }
public required string? UserName { get; set; }
}
public class Person
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTimeOffset? BirthDate { get; set; }
}
public class User
{
public required Guid Id { get; set; }
public required string? UserName { get; set; }
}
[UtilityType("Intersection<Person, User>")]
public partial class PersonAndUser;
// generates:
public partial class PersonAndUser
{
public Guid Id { get; set; }
}
If this gets at all popular, I'll add more compiler messages, syntax highlighting & error checking (red-squiggles!), etc.
I chose to use a string argument instead of more C#-like syntax to allow for a more compact syntax that is identical in nearly every case to the TypeScript syntax. Under the covers, the generator uses ANTLR with a simple grammar to do the parsing, and extending it to support more selectors is fairly trivial.
If there's demand for a more verbose syntax, I'll consider adding it (or you can submit a PR).