Mapped shapes
TypeScript can build a type out of another one - Pick<Customer, "Id" | "Name">, Omit<Customer, "PasswordHash">,
Partial<Customer>, Readonly<Customer>, A & B. C# has no such thing, and adding it on its own
wouldn't help much: a derived interface that nothing implements is dead weight.
Under IfItQuacks it isn't, because matching is structural. Mark a partial interface with
[DuckShape<TSource>] and the generator fills it with members derived from the source - and every type
carrying those members satisfies it, including the source itself, a DTO, and an object literal in a test.
public sealed class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public string PasswordHash { get; set; } = "";
}
[DuckShape<Customer>(Omit = [nameof(Customer.PasswordHash)], Readonly = true)]
public partial interface ICustomerView;
The generator emits:
public partial interface ICustomerView
{
int Id { get; }
string Name { get; }
string Email { get; }
}
Customer doesn't implement ICustomerView and never has to:
[DuckTyped]
public static string Render(ICustomerView view) => $"{view.Id} {view.Name} <{view.Email}>";
Render(new Customer()); // the entity
Render(new CustomerRow(2, "Donald", "d@example.com")); // a DTO
Render(new { Id = 3, Name = "Daisy", Email = "" }); // an object literal
The options
| Option | TypeScript | What it does |
|---|---|---|
Pick |
Pick<T, K> |
Derives only the named members. |
Omit |
Omit<T, K> |
Derives every member except the named ones. |
Optional |
Partial<T> |
Makes every derived property nullable. |
Readonly |
Readonly<T> |
Drops the setters of the derived properties and indexers. |
IncludeMethods |
- | Derives methods, events and indexers as well. |
| several attributes | A & B |
Intersects: every member of every source is derived into the same interface. |
Pick and Omit are mutually exclusive, and both take member names - use nameof so a rename keeps
working. An unknown name is a build error rather than a silently smaller interface.
[DuckShape<Customer>(Pick = [nameof(Customer.Id), nameof(Customer.Name)], Readonly = true)]
public partial interface ICustomerKey;
[DuckShape<Customer>(Omit = [nameof(Customer.PasswordHash)], Optional = true, Readonly = true)]
public partial interface ICustomerPatch; // int? Id, string? Name, string? Email
[DuckShape<IReadable>(IncludeMethods = true)]
[DuckShape<IWritable>(IncludeMethods = true)]
public partial interface ITextPort; // string Read(); void Write(string value);
Optional widens the declared member and matching follows: ICustomerPatch is satisfied both by a
Customer (whose Id is int, which converts to int?) and by a partial payload
(new { Id = (int?)null, Name = "Donald", Email = (string?)null }).
What it is good for
- Views and redaction.
Duck.As<ICustomerView>(customer)hands out a live view with the sensitive members simply absent from the type - no DTO, no copy, no mapping code. - Response shaping. One entity, several derived interfaces, each one a documented contract.
- Tests. An object literal satisfies a derived shape, so a
Pickof the three members a test cares about is all the fixture it needs. - Intersections. C# can't express
IReadable & IWritablewithout declaring a type that implements both; here the intersection is a shape anything fitting both satisfies.
Interaction with the rest of IfItQuacks
A derived interface is an ordinary interface, so everything works on it: [DuckTyped] parameters,
constrained duck typing, Duck.As, Duck.Stub, Duck.Merge and
Duck.To.
You can add members by hand; they are kept as written and not derived a second time.
[DuckShape<Customer>(Pick = [nameof(Customer.Name)])]
public partial interface ICustomerName
{
string Display { get; } // yours, kept as is
}
Limits
- The target must be a
partial interface, and so must every type around it (IFITQUACKS010). - Public fields of the source are not derived. Only properties are, plus methods, events and
indexers under
IncludeMethods. (A field on the argument still satisfies a derived property, as everywhere else in IfItQuacks.) - Static members, generic methods and
init-only setters are not derived. - A member whose type is less accessible than the interface is reported
(
IFITQUACKS011), as is an unknownPick/Omitname,PickandOmittogether, and a mapping that derives nothing. - Nested shaping is not done: a derived
Addressmember keeps its own type, it does not become anIAddressView. [DuckShape<T>]is a generic attribute, so the consuming project needs C# 11 or later.
Runnable examples live in samples/IfItQuacks.Sample.MappedShapes.