-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Using special decorators for assertions and constraints #60093
Comments
Duplicate of #54925 (the functionality, not the syntax). |
@guillaumebrunerie, the expressiveness of the proposed feature would include, but not be limited to, that for numeric ranges. I think that the expressiveness usefully possible in the lambda expressions would resemble that for class Widget
{
public w: number;
}
@constraint((obj: Item) => { return obj.z.length === 2 }, 'there must be 2 elements in the z array.')
@constraint((obj: Item) => { return obj.z[1] instanceof Widget }, 'the second element of z must be a Widget')
@constraint((obj: Item) => { return typeof obj.z[0] === 'number'}, 'the first element of z must be a number')
@constraint((obj: Item) => { return obj.z !== null }, 'the z array must not be null.')
@constraint((obj: Item) => { return obj.x > obj.y }, 'x must be greater than y')
@constraint((obj: Item) => { return obj.x <= 100 }, 'x must be less than or equal to 100.')
@constraint((obj: Item) => { return obj.x > 0 }, 'x must be greater than 0.')
class Item
{
public x: number;
public y: number;
public z: Array<any>;
} One scenario which interests me, in particular, is being able to utilize the fact that @constraint((obj: Example) => { return !obj.p || obj.q }, 'p implies q.')
class Example
{
public p: boolean;
public q: boolean;
} Something like that would be intended to mean that after an assertion or in a conditional branch involving A second scenario of interest is being able to constrain elements in an array or iterable, either specifying that there exists one element which satisfies a constraint or specifying that all elements must satisfy a constraint. @constraint((obj: Item2) => { return obj.ws.some((value: Widget) => value.w === 1) }, "one Widget in ws must have w equal to 1.")
@constraint((obj: Item2) => { return obj.ws.every((value: Widget) => value.w > 0) }, "every Widget in ws must have w greater than 0.")
@constraint((obj: Item2) => { return obj.ws.every((value: Widget) => value instanceof Widget) }, "every element in ws must be a Widget.")
@constraint((obj: Item2) => { return obj.ws.length > 0 }, "ws must have at least one element.")
@constraint((obj: Item2) => { return obj.ws !== null }, "ws must not be null.")
@constraint((obj: Item2) => { return obj.ws !== undefined }, "ws must be defined.")
class Item2
{
public ws: Array<Widget>;
} |
@AdamSobieski This seems way too vague and wide as a proposal, given that even range types seem very far from being implemented in Typescript. Your last use case can be implemented using a type with three different values instead of having two booleans: declare const pAndQ: "bothPAndQ" | "onlyQ" | "neitherPNorQ"; Then |
Thank you. I see your point about the timing of the proposal as numeric range types are still being discussed. I updated the previous comment to broach constraints on the elements of arrays and iterables. This proposal appears to be getting wider as I attempt to make it less vague. Also, for anyone interested in playing with the syntax ideas today, here is a prototype (ideas to improve it are welcomed): import assert = require("assert");
interface Validatable
{
validate(): void;
}
function constraint<TConstructor extends { new(...args: any[]): TType }, TType extends Validatable>(constraint: (arg: TType) => boolean, message: string): (ctor: TConstructor) => TConstructor
{
return function (constructor: TConstructor): TConstructor
{
if ('validate' in constructor.prototype)
{
const original = constructor.prototype.validate;
constructor.prototype.validate = function ()
{
original.call(this);
assert(constraint(this), new Error('[' + constructor.name + '] ' + message));
}
}
else
{
constructor.prototype.validate = function ()
{
assert(constraint(this), new Error('[' + constructor.name + '] ' + message));
}
}
return constructor;
};
}
class Widget
{
public w: number;
}
@constraint((obj: Item2) => { return obj.ws.some((value: Widget) => value.w === 1) }, "at least one Widget in ws must have w equal to 1.")
@constraint((obj: Item2) => { return obj.ws.every((value: Widget) => value.w > 0) }, "every Widget in ws must have w greater than 0.")
@constraint((obj: Item2) => { return obj.ws.every((value: Widget) => value instanceof Widget) }, "every element in ws must be a Widget.")
@constraint((obj: Item2) => { return obj.ws.length > 0 }, "ws must have at least one element.")
@constraint((obj: Item2) => { return obj.ws !== null }, "ws must not be null.")
@constraint((obj: Item2) => { return obj.ws !== undefined }, "ws must be defined.")
class Item2 implements Validatable
{
public ws: Array<Widget>;
public validate() { }
}
let item = new Item2();
let w0: Widget = new Widget();
w0.w = 1;
item.ws = new Array<Widget>();
item.ws[0] = w0;
item.validate(); |
@constraint((t: T) => { return t.x > 0 && t.x <= 100 }, "A T's x is between 1 and 100.")
@constraint((t: T) => { return t.y.length > 0 && t.y.length <= 4 }, "A T only has between 1 and 4 y's.")
type T =
{
x: number;
y: Array<any>;
} It's not clear when these constraints are supposed to be executed. If they behave like class decorators by injecting constraint applications into expressions of type Also what kind of type information do they provide? Are they supposed to be statically analyzable / executed by the type checker? If so, I suggest you reading through #52088 on why arbitrary programming in types is not a good idea. |
Thank you. I'm still reading and thinking about #52088. Initially, a
As an aside, I am recently thinking about the assured validity of objects, whether some objects can be always valid, and how to go about updating objects which are to be always valid using programming-language features such as setting the values of single properties, one at a time. Perhaps such always-valid objects could provide a method
or
To your question, yes, I was thinking that the contents of the lambda expressions in special decorators, e.g., |
I am excited to share that I revised the syntax and implementation sketch for this proposal. The rationale for the new decorator syntax is to support developers being able to use template strings for custom Now the proposed decorators add static members to constrained types, storing constraints in a static array on types. The No interfaces are required to be implemented by any constrained types. Also, import assert = require("assert");
function* getConstrainedTypes(type: { new(...args: any[]): any }): Iterable<{ new(...args: any[]): any }>
{
if (type !== undefined && type !== null)
{
const base = Object.getPrototypeOf(type);
for (const t of getConstrainedTypes(base))
{
yield t;
}
if (Object.hasOwn(type, 'constraints') === true)
{
yield type;
}
}
}
function constraint<TConstructor extends { new(...args: any[]): TType }, TType>(constraint: (arg: TType) => void): (type: any) => any
{
return function (type: any): any
{
if (Object.hasOwn(type, 'constraints') === false)
{
Object.defineProperty(type, 'constraints',
{
value: new Array<Function>(),
writable: false,
enumerable: true,
configurable: false
});
Object.defineProperty(type, 'validate',
{
value: function (obj: object)
{
let errors = new Array<Error>();
for (const t of getConstrainedTypes(this))
{
// @ts-ignore
for (const c of t.constraints)
{
try
{
c(obj);
}
catch (e)
{
errors.push(e);
}
}
}
if (errors.length == 1)
{
throw errors[0];
}
if (errors.length > 1)
{
throw new AggregateError(errors);
}
},
writable: false,
enumerable: true,
configurable: false
});
}
type.constraints.push(constraint);
return type;
}
}
function freeze(type: any): any
{
if (Object.hasOwn(type, 'constraints') === true)
{
Object.freeze(type.constraints);
}
return type;
}
@freeze
@constraint((obj: Foo) => { assert(obj.s >= 0) })
@constraint((obj: Foo) => { assert(obj.r >= 0) })
class Foo
{
r: number;
s: number;
}
@freeze
@constraint((obj: Foo2) => { assert(obj.u >= 0) })
@constraint((obj: Foo2) => { assert(obj.t >= 0) })
class Foo2 extends Foo
{
t: number;
u: number;
}
class Foo3 extends Foo2
{
v: number;
}
@freeze
@constraint((obj: Foo4) => { assert(obj.w >= 0) })
@constraint((obj: Foo4) => { assert(obj.v >= 0) })
class Foo4 extends Foo3
{
w: number
}
let obj = new Foo4();
obj.r = 6;
obj.s = 5;
obj.t = 4;
obj.u = 3;
obj.v = 2;
obj.w = 1;
// @ts-ignore
Foo4.validate(obj); Hopefully, this version is a bit more enjoyable to look at, to consider, and to explore. I have also created a repository for these ideas. Thank you. |
We don't want to run usercode as part of typechecking, and we can't subtype types created by predicates ("does this program return Doing this sort of thing at runtime is out of scope for TS; I recommend libraries like |
Understood. Thank you. |
π Search Terms
typescript, inference, decorators, assertions, constraints
β Viability Checklist
β Suggestion
I would like to request a feature involving the use of one or more special decorators for providing even more information about types, functions, and properties.
π Motivating Example
π» Use Cases
What do you want to use this for? Even more granular expressiveness about types. functions, and properties would be useful.
What shortcomings exist with current approaches? With one or more special decorators, developers could express many more useful assertions and constraints.
What workarounds are you using in the meantime? Workarounds are situation-dependent.
The text was updated successfully, but these errors were encountered: