microsoft / microsoft/typespec

Open union validation

Open
#3,363 1 comment 1 reaction 0 assignees View on GitHub
compiler:core design:needed triaged:core
Dominant language
Java
Stars
5.9k
Forks
394
Avg merge
1d 23h
Merged PRs (30d)
104

Description

# Differentiate open union where values are exhaustive at a point in type

How to differentiate an open union where certain emitters might know the whole set of values and want a more precise type

We estalished that when dealing with union of values that might return different values in the future to an existing API version we should always use an open union

```tsp
union Open {"a", "b", string }
```

This is an accurate representation of the API.

If a service can guarantee that the values will never change then they can use a closed union or an enum

```tsp
union Closed {"a", "b"}
enum Closed { a, b }
```

## Problem

Some emitters however would like to represent the more accurate type as they might know all the values and are the ones controlling it(Service) or just want a more precise type that won't affect the runtime(TypeScript client)

And we currently wouldn't be able to differentiate between an union that is open because it might change in the future and an union that currently allows extra values.

```tsp
union CurrentValues {"a", "b", string }
union WithKnownValues {"a", "b", string }
```

## Ideas

## Opt-in decorator

If a service is using this pattern to represent union that might change in the future it is probably the 99% case that those open union represent the set of current values and doesn't actually allow any values at the latest version. So having a decorator to opt-in to a more precise type would be very tedious as you'd most likely have to apply to to every union.

```tsp
@exhaustiveAtVersion
union CurrentValues {"a", "b", string }
union WithKnownValues {"a", "b", string }
```

However if a service doesn't actually care about this - control both end(client and service) or maybe even went and did the work to guarantee that values defined at a version never change(showhow) then the decorator above does seem like a good idea as if they actually use `union WithKnownValues {"a", "b", string }` it most likely mean they accept the extra values.

## Opt-in service and opt-out decorator

An alternative would be to opt-in a service to this behavior using a decorator on the service namespace

```tsp
@useExhaustiveOpenUnion
namespace MyService;

union CurrentValues { "a", "b", string }
@allowsUnknownValues
union WithKnownValues { "a", "b", string }
```

The emitter could be the one deciding to opt-in to this behavior with a flag as well

```yaml
options:
my-emitter:
open-union-handling: exhaustive | open
```

```tsp
namespace MyService;

union CurrentValues { "a", "b", string }
@allowsUnknownValues
union WithKnownValues { "a", "b", string }
```

```tsp
model Foo {
@pattern("[a-z]+")
name: string;
}
```

## Usage scenarios

1. If a single service version will only ever accept or return a fixed set, use fixed union, adding new values in new service versions is not a breaking change.

```tsp
enum OnlyOutput {a, b, @added(v2) c }
// In this case the service does guarantee that in v1 c would never get returned
```

2. If a single service version might return a new value in the future, use an extensible union, adding new values in new service versions is not a breaking change.

```tsp
enum OnlyOutput {a, b, @added(v2) c, string }
// in v1 we might get c
```

3. If a single service version validates today that an input is inside a fixed set of known values, but may allow new values in the future, use a fixed union. Accepting new values in the future is not a breaking change.

```tsp
enum OnlyUsedAsInput {a, b, @added(v2) c }
// sending c in v1 is invalid
```

4. If a single service version has an input AND output type that it validates on input but may accept and return new values in the future... I think this is the issue we are talking about.

```tsp
enum InputAndOutput {a, b, @added(v2) c, string }
// in v1 we might get
// but we want to validate at this point only "a", "b", "c" can be sent
```

1. Treat open union as an exhaustive list for validation and TypeScript emitter by default
2. Decorator to opt-out
3. Emitter flag to disable validation/behavior

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.