dart-lang / dart-lang/language

Proposal: `sealed` class members

Open
#4,041 5 comments 4 reactions 0 assignees View on GitHub
feature
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

## Overview

I recently learned that the `abstract` keyword can be used for class members:

```dart
abstract class Foo {
abstract final Object? a;
abstract Object? b;
}
```

This issue proposes using the `sealed` keyword in a similar way:

```dart
class Foo {
const Foo({this.a});
sealed final Object? a;
}

class Bar extends Foo {
Bar({super.a, this.b});
sealed Object? b;
}
```


Adding `sealed` to a class field prevents the field from being overridden.

```dart
class A {
const A({this.value});
sealed final int? value;
}

class B extends A {
@override
int get value => 42; // error
}
```


Detailed rules (click to expand)

#### Used for class fields

```dart
sealed int value = 42; // error: not in a class declaration

class A {
const A(this.value);
sealed final int value; // OK

void foo() {
sealed int value = 42; // error: does not apply to local variables
}
}

class C {
sealed int i = 0; // OK, can be used for non-final variables

late sealed Object data; // OK, works for "late" values

sealed void foo() {
// OK, can be used for methods
}

sealed int get value => 42; // OK, but getters won't gain benefits
// as described further down
sealed set value(int? newValue) {
// OK, works for setters
}
}
```

#### Does not apply to abstract or static fields

```dart
abstract class A {
abstract sealed Object value; // error: abstract sealed member

sealed String get label; // error: abstract sealed getter

sealed void foo(); // error: abstract sealed method

static sealed A of(BuildContext context) {
// error: static sealed member
}
}
```

#### Don't override inherited `sealed` fields

```dart
class A {
sealed void foo() {
print('I love Dart!');
}
}

class B extends A {
@override
void foo() { // error: cannot override an inherited sealed field
print('hello');
}
}
```

#### Can override non-`sealed` fields

```dart
class A {
const A({this.value});
final Object value;
}

class B implements A {
@override
sealed int value = 0; // OK
}

abstract class C {
Object? get data;
}

class D extends C {
const D({this.data});

@override
sealed final Object data; // OK
}
```

#### Implementing a class with a `sealed` field

A `sealed` class member can be overridden when implementing the class, but the new value must also have the `sealed` modifier. A `sealed` member cannot be replaced with a getter.

```dart
class A {
const A(this.value);
sealed final Object value;
}

class B implements A {
const B({this.value = ''});

@override
sealed final String value; // OK
}

class B implements A {
const B({this.value = ''});

@override
final String value; // error: value must have "sealed" modifier
}

class C implements A {
@override
sealed int value = 0; // OK
}

class D implements A {
@override
sealed int get value => 0; // error: member cannot be changed to getter
}
```

When the class is implemented, a `sealed` getter or method can be overridden by another `sealed` getter/method with no additional restrictions. To prevent overriding, use a `base class`.

```dart
import 'dart:math' as math;

class A {
sealed String get coinFlip {
return math.Random().nextBool() ? 'heads' : 'tails';
}
}

class B implements A {
@override
sealed String get coinFlip => 'always tails'; // OK
}

sealed class C {
sealed String get coinFlip {
// cannot be overridden, unless it's implemented in the same library
return math.Random().nextBool() ? 'heads' : 'tails';
}
}
```


## Benefits

### Type promotion

A `sealed` class member qualifies for type promotion, as if it were a local variable.

```dart
class A {
const A({this.value});
sealed final Object? value;

void foo() {
if (value is String) {
print(value.substring(2));
}
}
}

class B {
B({this.value});
sealed Object? value;

void foo() {
if (value is int) {
value += 3;
}
}
}
```

### Constant class fields

If `foo` is a constant value, and `bar` is a `sealed final` field in its class declaration, then `foo.bar` can be used in a constant context.

```dart
class Fraction {
const Fraction(this.numerator, this.denominator)
: value = numerator / denominator;

sealed final num numerator, denominator;
sealed final double value;
}

const fraction = Fraction(5, 4);
const remainder = fraction.value % 1;
```


## Discussion

This proposal is closely related to #1518, but has a few differences.

#### Advantages of `sealed`
- Uses existing keyword (`stable` could be a variable name)
- Supports type promotion for non-`final` variables
- Allows a class field to be used in a constant context (resolves #299)

#### Advantages of `stable`
- Supports type promotion for getters
- Since a `stable` getter can override a `stable` field, it can be used in place of a `late final` value, allowing a class declaration to keep its `const` constructor (though this could also be achieved via #2464)

Contributor guide

Open the contributing guide

Research direction

Start by reviewing this proposal alongside the related discussions in #1518, #299, and #2464. Determine whether the `sealed` member rules, type-promotion behavior, and constant-context behavior have an agreed design; done would require a decided language specification change rather than a small isolated edit.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.