-
-
Notifications
You must be signed in to change notification settings - Fork 231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal for data constructor defaults #2443
base: main
Are you sure you want to change the base?
Conversation
|
If we want to solve the problem of adding defaults to a generic constructor, I think we should consider both class constructors and "plain" constructors (such as import * as S from "@effect/schema/Schema"
import * as assert from "assert"
import * as Data from "effect/Data"
import { dual } from "effect/Function"
import type * as Types from "effect/Types"
/**
* Represents a plain constructor function.
*/
export interface PlainConstructor<In extends Array<any>, Out> {
(...input: In): Out
}
/**
* Represents a class constructor function.
*/
export interface ClassConstructor<In extends Array<any>, Out> {
new(...input: In): Out
}
/**
* Maps the input of a plain constructor function using a mapper function.
*/
export const mapPlainInput = <In extends Array<any>, Out, In2 extends Array<any>>(
c: PlainConstructor<In, Out>,
f: (...i2: In2) => In
): PlainConstructor<In2, Out> =>
(...i2) => c(...f(...i2))
/**
* Maps the input of a class constructor function using a mapper function.
*/
export const mapClassInput = <In extends Array<any>, Out, In2 extends Array<any>>(
c: ClassConstructor<In, Out>,
f: (...input: In2) => In
): ClassConstructor<In2, Out> =>
class extends (c as any) {
constructor(...input: In2) {
super(...f(...input))
}
} as any
/**
* Makes specific properties of the original type optional.
*/
export type PartialInput<In, D extends keyof In> = Types.Simplify<
Omit<In, D> & { [K in D]?: In[K] }
>
/**
* A shape for expressing defaults
*/
export type Defaults<In> = { [K in keyof In]?: () => In[K] }
/**
* Adds default values to properties of an object.
*/
export const addDefaults: {
<In, D extends Defaults<In>>(
defaults: D
): (partialInput: PartialInput<In, keyof D & keyof In>) => In
<In, D extends Defaults<In>>(
partialInput: PartialInput<In, keyof D & keyof In>,
defaults: D
): In
} = dual(2, (
partialInput: object | undefined,
defaults: Record<string, () => unknown>
) => {
const out: Record<string, unknown> = { ...partialInput }
for (const k in defaults) {
if (!Object.prototype.hasOwnProperty.call(out, k)) {
out[k] = defaults[k]()
}
}
return out
})
/**
* Adds default values to properties of a plain constructor's input object.
*/
export const addPlainConstructorDefaults = <Head, Tail extends Array<any>, Out, D extends Defaults<Head>>(
plainConstructor: PlainConstructor<[Head, ...Tail], Out>,
defaults: D
): PlainConstructor<[PartialInput<Head, keyof D & keyof Head>, ...Tail], Out> =>
mapPlainInput(plainConstructor, (head, ...tail) => [addDefaults<Head, D>(head, defaults), ...tail] as const)
/**
* Adds default values to properties of a class constructor's input object.
*/
export const addClassConstructorDefaults = <Head, Tail extends Array<any>, Out, D extends Defaults<Head>>(
classConstructor: ClassConstructor<[Head, ...Tail], Out>,
defaults: D
): ClassConstructor<[PartialInput<Head, keyof D & keyof Head>, ...Tail], Out> =>
mapClassInput(classConstructor, (head, ...tail) => [addDefaults<Head, D>(head, defaults), ...tail] as const)
// --------------------- EXAMPLES -------------------------------
// ----------------------------------------------------
// plain constructor
// ----------------------------------------------------
const plainConstructor: PlainConstructor<[{ a: string; b: number }, boolean], { a: string; b: number }> = (a) => a
const plainConstructorWithDefaults = addPlainConstructorDefaults(plainConstructor, { a: () => "" })
export type plainConstructorWithDefaultsParameters = Parameters<typeof plainConstructorWithDefaults>
/*
type plainConstructorWithDefaultsParameters = [{
b: number;
a?: string;
}, boolean]
*/
assert.deepStrictEqual(plainConstructorWithDefaults({ b: 1 }, true), { b: 1, a: "" })
assert.deepStrictEqual(plainConstructorWithDefaults({ b: 1, a: "a" }, true), { b: 1, a: "a" })
// ----------------------------------------------------
// Data.taggedEnum
// ----------------------------------------------------
const ctors = Data.taggedEnum<
| { readonly _tag: "BadRequest"; readonly status: 400; readonly message: string }
| { readonly _tag: "NotFound"; readonly status: 404; readonly message: string }
>()
const BadRequest = addPlainConstructorDefaults(ctors.BadRequest, { status: () => 400 })
assert.deepStrictEqual({ ...BadRequest({ message: "a" }) }, { _tag: "BadRequest", message: "a", status: 400 })
// ----------------------------------------------------
// Data Class
// ----------------------------------------------------
// add defaults to a class constructor manually
class DataClassWithDefaultsManual extends Data.Class<{ a: string; b: number }> {
constructor(props: PartialInput<DataClassWithDefaultsManual, "a">) {
super(addDefaults(props, { a: () => "" }))
}
}
export type DataClassWithDefaultsManualParameters = ConstructorParameters<typeof DataClassWithDefaultsManual>
/*
type DataClassWithDefaultsManualParameters = [props: {
readonly b: number;
readonly a?: string;
}]
*/
assert.deepStrictEqual({ ...new DataClassWithDefaultsManual({ b: 1 }) }, { a: "", b: 1 })
assert.deepStrictEqual({ ...new DataClassWithDefaultsManual({ b: 1, a: "a" }) }, { a: "a", b: 1 })
class DataClassWithAllDefaultsManual extends Data.Class<{ a: string; b: number }> {
constructor(props: PartialInput<DataClassWithAllDefaultsManual, "a" | "b"> | void) {
super(addDefaults(props ?? {}, { a: () => "", b: () => 1 }))
}
}
assert.deepStrictEqual({ ...new DataClassWithAllDefaultsManual() }, { a: "", b: 1 })
// add defaults to a class constructor via combinator
const DataClassWithDefaultsCombinator = addClassConstructorDefaults(
class DataClass extends Data.Class<{ a: string; b: number }> {},
{
a: () => ""
}
)
export type DataClassWithDefaultsCombinatorParameters = ConstructorParameters<
typeof DataClassWithDefaultsCombinator
>
/*
type DataClassWithDefaultsCombinatorParameters = [{
readonly b: number;
readonly a?: string;
}]
*/
assert.deepStrictEqual({ ...new DataClassWithDefaultsCombinator({ b: 1 }) }, { a: "", b: 1 })
assert.deepStrictEqual({ ...new DataClassWithDefaultsCombinator({ b: 1, a: "a" }) }, { a: "a", b: 1 })
// ----------------------------------------------------
// Schema Class
// ----------------------------------------------------
// add defaults to a class constructor manually
class SchemaClassWithDefaultsManual extends S.Class<SchemaClassWithDefaultsManual>("Person")({
a: S.string,
b: S.number
}) {
constructor(
props: PartialInput<SchemaClassWithDefaultsManual, "a">,
disableValidation?: boolean
) {
super(addDefaults(props, { a: () => "" }), disableValidation)
}
}
export type SchemaClassWithDefaultsManualParameters = ConstructorParameters<typeof SchemaClassWithDefaultsManual>
/*
type SchemaClassWithDefaultsManualParameters = [props: {
readonly b: number;
readonly a?: string;
}, disableValidation?: boolean | undefined]
*/
assert.deepStrictEqual({ ...new SchemaClassWithDefaultsManual({ b: 1 }, true) }, { a: "", b: 1 })
assert.deepStrictEqual({ ...new SchemaClassWithDefaultsManual({ b: 1, a: "a" }, true) }, { a: "a", b: 1 })
// add defaults to a class constructor via combinator
const SchemaClassWithDefaultsCombinator = addClassConstructorDefaults(
class SchemaClass extends S.Class<SchemaClass>("Person")({
a: S.string,
b: S.number
}) {},
{
a: () => ""
}
)
export type SchemaClassWithDefaultsCombinatorParameters = ConstructorParameters<
typeof SchemaClassWithDefaultsCombinator
>
/*
type SchemaClassWithDefaultsCombinatorParameters = [{
readonly b: number;
readonly a?: string;
}, disableValidation?: boolean | undefined]
*/
assert.deepStrictEqual({ ...new SchemaClassWithDefaultsCombinator({ b: 1 }, true) }, { a: "", b: 1 })
assert.deepStrictEqual({ ...new SchemaClassWithDefaultsCombinator({ b: 1, a: "a" }, true) }, { a: "a", b: 1 }) |
@gcanti totally. One thing to keep in mind is that in the Schema class example you are loosing the benefit of a class combined value and shape in one, as well as the |
@patroza do you mean class SchemaClassWithDefaultsCombinatorClass extends addClassConstructorDefaults(
class SchemaClass extends S.Class<SchemaClass>("Person")({
a: S.string,
b: S.number
}) {},
{
a: () => ""
}
) {} It doesn't work anyway // ts error: Property 'ast' does not exist on type 'typeof SchemaClassWithDefaultsCombinatorClass'.ts(2339)
SchemaClassWithDefaultsCombinatorClass.ast |
@gcanti no, it should work like class TestError extends Data.addDefaults(
Data.TaggedError("TestError")<{ a: string; b: number }>,
{ b: () => 1 }
) {} so class SchemaClass extends addClassConstructorDefaults(S.Class<SchemaClass>("Person")({
a: S.string,
b: S.number
}),
{
a: () => ""
}
) {} otherwise you have options like default property initialisers in the class, or using the manual approach. |
It doesn't work either, or at least not in my setup. Does it work for you? class SchemaClass extends addClassConstructorDefaults(
S.Class<SchemaClass>("Person")({
a: S.string,
b: S.number
}),
{
a: () => ""
}
) {}
// Property 'ast' does not exist on type 'typeof SchemaClass'.ts(2339)
SchemaClass.ast |
@gcanti I bet, but it's how it should work, imo. class X extends addClassConstructorDefaults(
class extends S.Class.. {
.. some more custom stuff
}) {} |
I don't see how it could ever work, in your combinator (and in my export const addDefaults: <Args, Defaults extends { [K in keyof Args]?: () => Args[K] }, Out extends object>(
newable: new(args: Args) => Out,
defaults: Defaults
) => new(
args: Omit<Args, keyof Defaults> & { [K in keyof Args as K extends keyof Defaults ? K : never]?: Args[K] }
) => Out all the rest that possibly defines the class (methods, static fields, etc...) is ignored and therefore is not inherited by the result (at the type-level). Something like this seems to work export type ClassConstructorHead<C> = C extends ClassConstructor<[infer Head], any> ? Head
: never
export type ClassConstructorTail<C> = C extends ClassConstructor<[any, ...infer Tail], any> ? Tail
: never
export type ClassConstructorOut<C> = C extends ClassConstructor<any, infer Out> ? Out : never
export declare const addClassConstructorDefaults2: <
C extends ClassConstructor<any, any>,
D extends Defaults<ClassConstructorHead<C>>
>(
classConstructor: C,
defaults: D
) =>
& C
& ClassConstructor<
[
PartialInput<ClassConstructorHead<C>, keyof D & keyof ClassConstructorHead<C>>,
...ClassConstructorTail<C>
],
ClassConstructorOut<C>
>
class SchemaClass extends addClassConstructorDefaults2(
S.Class<SchemaClass>("Person")({
a: S.string,
b: S.number
}),
{
a: () => ""
}
) {}
// OK
SchemaClass.ast |
@gcanti you're right, that's an oversight in my proposal, which was only targeting the Data classes, and sadly only the instance side of the type, that is my bad. |
Related to Schema #2319
and issue #1997