Signal Forms should not enforce native `min` / `max` / `minLength` / `maxLength` types on custom controls
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 54/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- angular, typescript
- 領域
- frontend
調査の方向性
packages/forms/signals/src/api/control.ts の min、max、minLength、maxLength の定義から始め、リンクされた StackBlitz の例で型エラーを再現します。bindings をどのように扱うべきかを決める前に、関連する issue #65676 と既存の custom-control の型付けを確認します。custom controls で型の異なる入力を、これらのコンパイル時エラーなしに formField とともに使用でき、native-control の動作が引き続き理解され、関連するテストでカバーされていれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
Signal Forms type-check the min / max / minLength / maxLength inputs of a control against the native HTML meaning of those names:
For a native <input> this is sound: min really is "the smallest acceptable value" and minLength really is "the smallest number of characters".
For a custom control it is an assumption the framework cannot verify. Component libraries have had inputs with these names for years — since long before Signal Forms existed — and there is no reason why a minLength input on someone's component has to mean the same thing as minLength on <input>. Today that assumption is enforced at compile time, so a control whose min / minLength means something else simply cannot be used with [formField] — even when the schema declares no min() / minLength() rule at all.
Two real examples from Taiga UI:
1. Range / InputRange — a slider with two thumbs. Its value is readonly [number, number], but min / max are the bounds of the scale, and they are plain numbers:
export class TuiInputRange implements FormValueControl<readonly [number, number]> {
min = input(0);
max = input(100);
}
<!-- Type 'readonly [number, number] | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'. -->
<tui-input-range [formField]="f.range" />
~~~~~~~~~
Signal Forms require them to accept readonly [number, number] | undefined — a pair where the control needs a single number.
2. InputDateRange — a date range picker. Its minLength is the minimal length of the range (3 days, 2 months, …), not a number of characters:
// type TuiDayLike = {day?: number; month?: number; year?: number}
minLength = input<TuiDayLike | null>(null);
min = input<TuiDay>(TUI_FIRST_DAY); // earliest selectable day
<!--
[min] / [max] props:
Type 'TuiDayRange | undefined' is not assignable to type 'TuiDay | null'.
Type 'undefined' is not assignable to type 'TuiDay | null'.
[minLength] / [maxLength] props:
Type 'number | undefined' is not assignable to type 'TuiDayLike | null'.
Type 'undefined' is not assignable to type 'TuiDayLike | null'.
-->
<tui-textfield>
<input tuiInputDateRange [formField]="f.dates" />
~~~~~~~~~
</tui-textfield>
Signal Forms require minLength to accept number | undefined and min to accept NonNullable<TValue> | undefined, i.e. TuiDayRange | undefined — the whole range as the minimum, which is meaningless for this control.
Other examples across the library:
| control | its value type | its input means | what Signal Forms require input-prop to accept |
|---|---|---|---|
tuiTextarea |
string |
min / max: number of rows (number) |
string | undefined |
tuiInputDateMulti |
TuiDay[] |
min / max: selectable day range (TuiDay) |
TuiDay[] | undefined |
None of these can be "typed correctly" — the meanings do not overlap. The only way out is to accept the framework's type and throw the value away in a transform, which is exactly the kind of code the type system is supposed to prevent.
Proposed change
Do not type-check min / max / minLength / maxLength bindings for custom controls — accept whatever the control declares (e.g. unknown on the write side, the way InputSignalWithTransform<T, unknown> already does for the transform case), or make the binding opt-in so that a control states explicitly that its input carries the native meaning.
The runtime side deserves the same question, since the value is written into the input regardless of what it means there, but the type check is the part that blocks adoption today: it fails even for forms that never use these validators.
Please provide a link to a minimal reproduction of the bug
Please provide the exception or error you saw
<!-- Type 'readonly [number, number] | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'. -->
<tui-input-range [formField]="f.range" />
~~~~~~~~~
<!-- Type 'TuiDayRange | undefined' is not assignable to type 'TuiDay | null'. (min / max)
Type 'number | undefined' is not assignable to type 'TuiDayLike | null'. (minLength / maxLength) -->
<tui-textfield>
<input tuiInputDateRange [formField]="f.range" />
~~~~~~~~~
</tui-textfield>
Please provide the environment you discovered this bug in (run ng version)
Angular CLI : 22.0.7
Angular : 22.0.8
Node.js : 26.4.0
Package Manager : npm 11.17.0
Operating System : darwin arm64
┌───────────────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────────────┼───────────────────┼───────────────────┤
│ @angular-devkit/build-angular │ 22.1.3 │ 22.1.3 │
│ @angular-devkit/core │ 22.1.3 │ 22.1.3 │
│ @angular/build │ 22.0.7 │ 22.0.7 │
│ @angular/cdk │ 22.0.6 │ 22.0.6 │
│ @angular/cli │ 22.0.7 │ 22.0.7 │
│ @angular/common │ 22.0.8 │ 22.0.8 │
│ @angular/compiler │ 22.0.8 │ 22.0.8 │
│ @angular/compiler-cli │ 22.0.8 │ 22.0.8 │
│ @angular/core │ 22.0.8 │ 22.0.8 │
│ @angular/forms │ 22.0.8 │ 22.0.8 │
│ @angular/platform-browser │ 22.0.8 │ 22.0.8 │
│ @angular/platform-browser-dynamic │ 22.0.8 │ 22.0.8 │
│ @angular/router │ 22.0.8 │ 22.0.8 │
│ rxjs │ 7.8.2 │ 7.8.2 │
│ typescript │ 6.0.3 │ ~6.0.2 │
│ vitest │ 4.1.11 │ ^4.0.8 │
│ zone.js │ 0.15.1 │ ~0.15.0 │
└───────────────────────────────────┴───────────────────┴───────────────────┘
Anything else?
Related
This continues
where min / max were already relaxed once — from number to NonNullable<TValue> | undefined — for exactly this reason: an existing ControlValueAccessor could not be migrated without breaking its public min / max API.
- 主要言語
- TypeScript
- スター
- 101k
- フォーク
- 28.1k
- 平均マージ
- 2日 6時間
- マージ済み PR(30日)
- 307
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
angular/angular のほかの issue
-
area: docs
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
-
area: forms forms: signals
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
-
area: docs gemini-triaged
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
-
area: forms forms: signals
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
-
area: docs area: forms
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
angular/angular の issue をすべて見る
似ている issue
-
VerificationGate: ATTRIBUTION quote guard never matches a normal quotation (\b around the quote) オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
danielmiessler/LifeOS#2234 ·
-
T: Bug
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
-
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
-
Mend: dependency security vulnerability untriaged
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100