Skip to content

Commit b45cc56

Browse files
committed
sv
1 parent 1c1fe31 commit b45cc56

File tree

7 files changed

+106
-27
lines changed

7 files changed

+106
-27
lines changed

src/debouncedState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type OriginalInfo<V> = Pick<IState<V>, 'activated' | 'touched' | 'error' | 'ownE
1313
* The state for debounce purpose.
1414
* Changes from the original state (`$`) will be debounced.
1515
*/
16-
export class DebouncedState<S extends IState<V>, V = ValueOf<S>> extends ValidatableState<V> implements IState<V> {
16+
export class DebouncedState<S extends IState<V>, V = ValueOf<S>, SV extends V = V> extends ValidatableState<V, SV> implements IState<V, SV> {
1717

1818
/**
1919
* The original state.
@@ -136,7 +136,7 @@ export class DebouncedState<S extends IState<V>, V = ValueOf<S>> extends Validat
136136
* The field state with debounce.
137137
* Value changes from `onChange` will be debounced.
138138
*/
139-
export class DebouncedFieldState<V> extends DebouncedState<FieldState<V>, V> {
139+
export class DebouncedFieldState<V, SV extends V = V> extends DebouncedState<FieldState<V, SV>, V, SV> {
140140
constructor(initialValue: V, delay = defaultDelay) {
141141
super(new FieldState(initialValue), delay)
142142
}

src/fieldState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ValidatableState } from './state'
55
/**
66
* The state for a field.
77
*/
8-
export class FieldState<V> extends ValidatableState<V> implements IState<V> {
8+
export class FieldState<V, SV extends V = V> extends ValidatableState<V, SV> implements IState<V, SV> {
99

1010
@observable.ref value!: V
1111

src/formState.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { observable, computed, isObservable, action, reaction, makeObservable, override } from 'mobx'
2-
import { IState, ValidateStatus, ValidateResult, ValueOfStatesObject } from './types'
2+
import { IState, ValidateStatus, ValidateResult, ValueOfStatesObject, SafeValueOfStatesObject } from './types'
33
import { ValidatableState } from './state'
44

5-
abstract class AbstractFormState<T, V> extends ValidatableState<V> implements IState<V> {
5+
abstract class AbstractFormState<T, V, SV extends V = V> extends ValidatableState<V, SV> implements IState<V, SV> {
66

77
/** Reference of child states. */
88
abstract readonly $: T
@@ -66,7 +66,7 @@ abstract class AbstractFormState<T, V> extends ValidatableState<V> implements IS
6666
this.resetChildStates()
6767
}
6868

69-
override async validate(): Promise<ValidateResult<V>> {
69+
override async validate(): Promise<ValidateResult<SV>> {
7070
if (this.disabled) {
7171
return this.validateResult
7272
}
@@ -113,7 +113,7 @@ export type StatesObject = { [key: string]: IState }
113113
export class FormState<
114114
TStates extends StatesObject
115115
> extends AbstractFormState<
116-
TStates, ValueOfStatesObject<TStates>
116+
TStates, ValueOfStatesObject<TStates>, SafeValueOfStatesObject<TStates>
117117
> {
118118

119119
@observable.ref readonly $: Readonly<TStates>
@@ -171,9 +171,9 @@ export class FormState<
171171
* The state for a array form (list of child states).
172172
*/
173173
export class ArrayFormState<
174-
V, T extends IState<V> = IState<V>
174+
V, SV extends V = V, T extends IState<V, SV> = IState<V, SV>
175175
> extends AbstractFormState<
176-
readonly T[], V[]
176+
readonly T[], V[], SV[]
177177
> {
178178

179179
@observable.ref protected childStates: T[]

src/index.spec.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FieldState, FormState, ArrayFormState, TransformedState, DebouncedState, DebouncedFieldState } from '.'
2-
import { defaultDelay, delay } from './testUtils'
2+
import { assertType, defaultDelay, delay } from './testUtils'
33

44
describe('FieldState', () => {
55
it('should be newable', () => {
@@ -126,3 +126,63 @@ describe('Composition', () => {
126126
expect(hostState.error).toBe('empty hostname')
127127
})
128128
})
129+
130+
interface HostInput {
131+
hostname: string | null
132+
port: number | null
133+
}
134+
135+
function parseHost(input: string): HostInput {
136+
const [hostname, portStr] = input.split(':')
137+
const port = parseInt(portStr, 10)
138+
return { hostname, port }
139+
}
140+
141+
function stringifyHost(host: HostInput) {
142+
const suffix = (host.port == null || Number.isNaN(host.port)) ? '' : `:${host.port}`
143+
return host.hostname + suffix
144+
}
145+
146+
function createRawHostState(host: HostInput) {
147+
const hostnameState = new FieldState<string | null>(host.hostname).withValidator<string>(
148+
v => !v && 'empty hostname'
149+
)
150+
const portState = new FieldState<number | null, number>(host.port)
151+
return new FormState({
152+
hostname: hostnameState,
153+
port: portState
154+
})
155+
}
156+
157+
function createDebouncedHostState(hostStr: string) {
158+
const host = parseHost(hostStr)
159+
const rawState = createRawHostState(host)
160+
const state = new DebouncedState(
161+
new TransformedState(rawState, stringifyHost, parseHost),
162+
defaultDelay
163+
).withValidator(
164+
v => !v && 'empty'
165+
)
166+
return state
167+
}
168+
169+
describe('safeValue', () => {
170+
it('should work well', () => {
171+
const state = new FieldState<string | null>('foo').withValidator<string>(
172+
v => v == null && 'empty'
173+
)
174+
assertType<string>(state.safeValue)
175+
})
176+
it('should work well with multiple validators', () => {
177+
const state = new FieldState<string | number | null>('foo').withValidator<string | number>(
178+
v => v == null && 'empty'
179+
).withValidator<string>(
180+
v => typeof v !== 'string' && 'not string'
181+
)
182+
assertType<string>(state.safeValue)
183+
})
184+
it('should work well with complex states', () => {
185+
const rawHostState = createRawHostState({ hostname: 'foo', port: 80 })
186+
assertType<{ hostname: string, port: number }>(rawHostState.safeValue)
187+
})
188+
})

src/state.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,18 @@ export abstract class BaseState extends Disposable implements Pick<
3737
}
3838

3939
/** Extraction for State's validating logic */
40-
export abstract class ValidatableState<V> extends BaseState implements IState<V> {
40+
export abstract class ValidatableState<V, SV extends V> extends BaseState implements IState<V, SV> {
4141

4242
abstract value: V
4343
abstract touched: boolean
4444
abstract onChange(value: V): void
4545
abstract set(value: V): void
4646

47+
@computed get safeValue(): SV {
48+
if (!this.validated || this.hasError) throw new Error('TODO')
49+
return this.value as SV
50+
}
51+
4752
/** The original validate status (regardless of `validationDisabled`) */
4853
@observable protected _validateStatus: ValidateStatus = ValidateStatus.NotValidated
4954

@@ -76,9 +81,9 @@ export abstract class ValidatableState<V> extends BaseState implements IState<V>
7681
/** List of validator functions. */
7782
@observable.shallow private validatorList: Validator<V>[] = []
7883

79-
@action withValidator(...validators: Validator<V>[]) {
84+
@action withValidator<NSV = this['safeValue']>(...validators: Validator<V>[]): (this & { safeValue: NSV }) {
8085
this.validatorList.push(...validators)
81-
return this
86+
return this as (this & { safeValue: NSV })
8287
}
8388

8489
/** Current validation info. */
@@ -108,15 +113,15 @@ export abstract class ValidatableState<V> extends BaseState implements IState<V>
108113
})()
109114
}
110115

111-
@computed protected get validateResult(): ValidateResult<V> {
116+
@computed protected get validateResult(): ValidateResult<SV> {
112117
return (
113118
this.error
114-
? { hasError: true, error: this.error } as const
115-
: { hasError: false, value: this.value } as const
119+
? { hasError: true, error: this.error }
120+
: { hasError: false, value: this.value as SV }
116121
)
117122
}
118123

119-
async validate(): Promise<ValidateResult<V>> {
124+
async validate(): Promise<ValidateResult<SV>> {
120125
if (this.disabled) {
121126
return this.validateResult
122127
}

src/transformedState.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { computed } from 'mobx'
22
import { BaseState } from './state'
33
import { IState, Validator, ValueOf } from './types'
44

5-
export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>> extends BaseState implements IState<V> {
5+
export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>, SV extends V = V> extends BaseState implements IState<V, SV> {
66

77
/** The original state, whose value will be transformed. */
88
public $: S
@@ -23,6 +23,11 @@ export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>> extends
2323
return this.parseOriginalValue(this.$.value)
2424
}
2525

26+
@computed get safeValue() {
27+
if (!this.validated || this.hasError) throw new Error('TODO')
28+
return this.value as SV
29+
}
30+
2631
@computed get ownError() {
2732
return this.$.ownError
2833
}
@@ -46,7 +51,7 @@ export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>> extends
4651
async validate() {
4752
const result = await this.$.validate()
4853
if (result.hasError) return result
49-
return { ...result, value: this.value }
54+
return { ...result, value: this.value as SV }
5055
}
5156

5257
set(value: V) {
@@ -61,12 +66,12 @@ export class TransformedState<S extends IState<$V>, V, $V = ValueOf<S>> extends
6166
this.$.reset()
6267
}
6368

64-
withValidator(...validators: Array<Validator<V>>) {
69+
withValidator<NSV = SV>(...validators: Array<Validator<V>>): (this & { safeValue: NSV }) {
6570
const rawValidators = validators.map(validator => (
6671
(rawValue: $V) => validator(this.parseOriginalValue(rawValue))
6772
))
6873
this.$.withValidator(...rawValidators)
69-
return this
74+
return this as (this & { safeValue: NSV })
7075
}
7176

7277
disableWhen(predict: () => boolean) {

src/types.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ export type ValidateResultWithValue<T> = { hasError: false, value: T }
2525
export type ValidateResult<T> = ValidateResultWithError | ValidateResultWithValue<T>
2626

2727
/** interface for State */
28-
export interface IState<V = unknown> {
28+
export interface IState<Value = unknown, SafeValue extends Value = Value> {
2929
/** Value in the state. */
30-
value: V
30+
value: Value
31+
safeValue: SafeValue
3132
/** If value has been touched. */
3233
touched: boolean
3334
/** The error info of validation. */
@@ -50,15 +51,15 @@ export interface IState<V = unknown> {
5051
*/
5152
validated: boolean
5253
/** Fire a validation behavior. */
53-
validate(): Promise<ValidateResult<V>>
54+
validate(): Promise<ValidateResult<this['safeValue']>>
5455
/** Set `value` on change event. */
55-
onChange(value: V): void
56+
onChange(value: Value): void
5657
/** Set `value` imperatively. */
57-
set(value: V): void
58+
set(value: Value): void
5859
/** Reset to initial status. */
5960
reset(): void
6061
/** Append validator(s). */
61-
withValidator(...validators: Array<Validator<V>>): this
62+
withValidator<NSV = SafeValue>(...validators: Array<Validator<Value>>): (this & { safeValue: NSV })
6263
/**
6364
* Configure when state should be disabled, which means:
6465
* - corresponding UI is invisible or disabled
@@ -71,6 +72,9 @@ export interface IState<V = unknown> {
7172
dispose(): void
7273
}
7374

75+
/** Safe Value of `IState` */
76+
export type SafeValueOf<S> = S extends IState ? S['safeValue'] : never
77+
7478
/** Function to do dispose. */
7579
export interface Disposer {
7680
(): void
@@ -81,6 +85,11 @@ export type ValueOfStatesObject<StatesObject> = {
8185
[K in keyof StatesObject]: ValueOf<StatesObject[K]>
8286
}
8387

88+
/** Safe Value of states object. */
89+
export type SafeValueOfStatesObject<StatesObject> = {
90+
[K in keyof StatesObject]: SafeValueOf<StatesObject[K]>
91+
}
92+
8493
/** Value of `IState` */
8594
export type ValueOf<S> = S extends IState<infer V> ? V : never
8695

0 commit comments

Comments
 (0)