Skip to content

Commit

Permalink
Make Optional#type public
Browse files Browse the repository at this point in the history
  • Loading branch information
jviide committed Dec 2, 2024
1 parent a03647c commit 4b0a837
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-berries-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@badrap/valita": patch
---

Make Optional#type public
117 changes: 69 additions & 48 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,52 +542,37 @@ abstract class AbstractType<Output = unknown> {
return value;
}

/**
* Return new optional type that can not be used as a standalone
* validator. Rather, it's meant to be used as a with object validators,
* to mark one of the object's properties as _optional_. Optional property
* types accept both the original type, `undefined` and missing properties.
*
* The optional `defaultFn` function, if provided, will be called each
* time a value that is missing or `undefined` is parsed.
*
* @param [defaultFn] - An optional function returning the default value.
*/
// Use `<X extends T>() => X` instead of `() => T` to make literal
// inference work when an optionals with defaultFn is used as a
// ObjectType property.
// The same could be accomplished by replacing the `| T` in the
// output type with `NoInfer<T>`, but it's supported only from
// TypeScript 5.4 onwards.
optional<T extends Literal>(
abstract optional<T extends Literal>(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
defaultFn: <X extends T>() => X,
): Type<Exclude<Output, undefined> | T>;
// Support parsers like `v.array(t).optional(() => [])`
// so that the output type is `Infer<typeof t>[]` instead of
// `Infer<typeof t>[] | never[]`.
optional(
abstract optional(
defaultFn: () => Exclude<Output, undefined>,
): Type<Exclude<Output, undefined>>;
optional<T>(defaultFn: () => T): Type<Exclude<Output, undefined> | T>;
optional(): Optional<Output>;
/**
* Return new optional type that can not be used as a standalone
* validator. Rather, it's meant to be used as a with object validators,
* to mark one of the object's properties as _optional_. Optional property
* types accept both the original type, `undefined` and missing properties.
*
* The optional `defaultFn` function, if provided, will be called each
* time a value that is missing or `undefined` is parsed.
*
* @param [defaultFn] - An optional function returning the default value.
*/
optional<T>(
defaultFn?: () => T,
): Type<Exclude<Output, undefined> | T> | Optional<Output> {
// If this type is already Optional there's no need to wrap it inside
// a new Optional instance.
const optional =
this.name === "optional"
? (this as unknown as Optional<Output>)
: new Optional(this);

if (!defaultFn) {
return optional;
}
return new TransformType(optional, (v) => {
return v === undefined ? { ok: true, value: defaultFn() } : undefined;
});
}
abstract optional<T>(
defaultFn: () => T,
): Type<Exclude<Output, undefined> | T>;
abstract optional(): Optional<Output>;

/**
* @deprecated Instead of `.default(x)` use `.optional(() => x)`.
Expand Down Expand Up @@ -657,10 +642,6 @@ abstract class AbstractType<Output = unknown> {
);
}

map<T extends Literal>(
func: (v: Output, options: ParseOptions) => T,
): Type<T>;
map<T>(func: (v: Output, options: ParseOptions) => T): Type<T>;
/**
* Derive a new validator that uses the provided mapping function to
* perform custom mapping for the source validator's output values.
Expand All @@ -680,19 +661,17 @@ abstract class AbstractType<Output = unknown> {
*
* @param func - The mapping function.
*/
map<T extends Literal>(
func: (v: Output, options: ParseOptions) => T,
): Type<T>;
map<T>(func: (v: Output, options: ParseOptions) => T): Type<T>;
map<T>(func: (v: Output, options: ParseOptions) => T): Type<T> {
return new TransformType(this, (v, options) => ({
ok: true,
value: func(v as Output, options),
}));
}

chain<T extends Literal>(
func: (v: Output, options: ParseOptions) => ValitaResult<T>,
): Type<T>;
chain<T>(
func: (v: Output, options: ParseOptions) => ValitaResult<T>,
): Type<T>;
/**
* Derive a new validator that uses the provided mapping function to
* perform custom parsing for the source validator's output values.
Expand Down Expand Up @@ -720,6 +699,12 @@ abstract class AbstractType<Output = unknown> {
*
* @param func - The parsing function.
*/
chain<T extends Literal>(
func: (v: Output, options: ParseOptions) => ValitaResult<T>,
): Type<T>;
chain<T>(
func: (v: Output, options: ParseOptions) => ValitaResult<T>,
): Type<T>;
chain<T>(
func: (v: Output, options: ParseOptions) => ValitaResult<T>,
): Type<T> {
Expand Down Expand Up @@ -752,6 +737,27 @@ type TypeName =
abstract class Type<Output = unknown> extends AbstractType<Output> {
abstract name: TypeName;

optional<T extends Literal>(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
defaultFn: <X extends T>() => X,
): Type<Exclude<Output, undefined> | T>;
optional(
defaultFn: () => Exclude<Output, undefined>,
): Type<Exclude<Output, undefined>>;
optional<T>(defaultFn: () => T): Type<Exclude<Output, undefined> | T>;
optional(): Optional<Output>;
optional(defaultFn?: () => unknown): unknown {
// If this type is already Optional there's no need to wrap it inside
// a new Optional instance.
const optional = new Optional(this);
if (!defaultFn) {
return optional;
}
return new TransformType(optional, (v) => {
return v === undefined ? { ok: true, value: defaultFn() } : undefined;
});
}

/**
* Return new validator that accepts both the original type and `null`.
*/
Expand Down Expand Up @@ -848,15 +854,30 @@ class Nullable<Output = unknown> extends Type<Output | null> {
class Optional<Output = unknown> extends AbstractType<Output | undefined> {
readonly name = "optional";

constructor(
/** @internal */
private readonly _type: AbstractType<Output>,
) {
constructor(readonly type: Type<Output>) {
super();
}

optional<T extends Literal>(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
defaultFn: <X extends T>() => X,
): Type<Exclude<Output, undefined> | T>;
optional(
defaultFn: () => Exclude<Output, undefined>,
): Type<Exclude<Output, undefined>>;
optional<T>(defaultFn: () => T): Type<Exclude<Output, undefined> | T>;
optional(): Optional<Output>;
optional(defaultFn?: () => unknown): unknown {
if (!defaultFn) {
return this;
}
return new TransformType(this, (v) => {
return v === undefined ? { ok: true, value: defaultFn() } : undefined;
});
}

_createMatcher(): TaggedMatcher {
const matcher = this._type._matcher;
const matcher = this.type._matcher;

return taggedMatcher(TAG_OPTIONAL, (v, flags) =>
v === undefined || flags & FLAG_MISSING_VALUE
Expand All @@ -868,7 +889,7 @@ class Optional<Output = unknown> extends AbstractType<Output | undefined> {
_toTerminals(func: (t: TerminalType) => void): void {
func(this);
func(undefined_() as TerminalType);
this._type._toTerminals(func);
this.type._toTerminals(func);
}
}

Expand Down

0 comments on commit 4b0a837

Please sign in to comment.