-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Combining destructuring with parameter properties #5326
Comments
I like this. |
Note that in those proposals the parameter property modifier applied to the entire binding pattern (simpler) as opposed to the original suggestion here which in theory supports different visibility modifiers per destructured element (not sure that level of specificity would be worth it). |
@danquirk For me, if it's easier for you to do the other way, I don't really care. In fact, that was my first try (e.g It would be great if we could support both (since not always we want to create properties for all the parameters, and when we want, it would be simpler to use Probably it's something that people will use more and more, since destructuring is awesome. |
just ran into this. I think either approach would satisfy most use cases. Hope to see this in a future version. |
The first thing I tried was:
Something like this would be a nice-to-have. |
Definitely +1 this. The alternative tends to be... bulky. |
👍 |
3 similar comments
👍 |
+1 |
+1 |
+1 |
2 similar comments
👍 |
+1 |
Please use the GitHub reactions feature rather than standalone upvote comments. Thanks! |
In an attempt to be more DRY using named args and strong types (until something like your proposal lands), I tried this: interface ExampleArgs {
firstArg: string;
otherArg: number;
}
export default class Example implements ExampleArgs {
firstArg;
otherArg;
constructor(kwargs:ExampleArgs) {
return Object.assign(this, kwargs);
}
} but got |
Write this instead interface ExampleArgs {
firstArg: string;
otherArg: number;
}
export default class Example {
constructor(kwargs:ExampleArgs) {
return Object.assign(this, kwargs);
}
}
export interface Example extends ExampleArgs { } |
Thanks. I had to separate the exports from the declarations to make that work: interface ExampleArgs {
firstArg: string;
otherArg: number;
}
class Example {
constructor(kwargs:ExampleArgs) {
return Object.assign(this, kwargs);
}
}
export default Example;
interface Example extends ExampleArgs { } |
This would be incredibly useful for hydrating class-based models from JSON. As in export interface PersonDto {
name?: string;
}
export class Person {
constructor(public {name}: PersonDto = {}) {
}
} |
Meanwhile we get this feature, here's the workaround: export class PersonSchema {
firstName: string;
lastName: string;
email?: string; // Thanks to TypeScript 2, properties can be optional ;)
}
export class Person extends PersonSchema {
constructor(args: PersonSchema = {}) {
Object.assign(this, args);
}
} The side effect is that if This will also be your copy constructor. |
Accepting PRs to implement |
@RyanCavanaugh, you're proposing that constructor(public { name1 = value1, name2 = value2 }: { name1: string, name2: string }) {} would desugar to constructor({ name1 = value1, name2 = value2 }) {
this.name1 = name1;
this.name2 = name2;
} right? |
Why is this discussion only about constructors? Wouldn't this apply to other methods as well?!? |
I find this combination of declaration merging and interface ClsAttrs {
name: string;
}
interface Cls extends ClsAttrs {}
class Cls {
constructor(attrs: ClsAttrs) {
Object.assign(this, attrs)
}
}
export default Cls It's not great either because maybe you want to make certain props optional in the constructor arguments but required as a class' attribute. For some reason exporting |
Unfortunately, ts does not support mixing destructuring and parameter properties right now (see microsoft/TypeScript#5326). This still imporves maintainability. Addresses MIT-166
Still want this in 2022 |
I'd love to have a Dart-like syntax for this. We'd be able to do: class Test {
constructor({
private param1: string,
protected param2: number,
public param3?: string // optional
}); // <-- Might as well make the constructor branckets optional too!
}
const test = new Test({
param1: 'hello',
param2: 1337,
param3: 'world',
});
console.log(test.param3); // -> 'world' |
@larssn that is what I proposed here: But to be honest, I haven't used classes in JS/TS in a while. And not planning to use it again anytime soon. interface Obj {
readonly foo: string;
readonly bar: string;
}
function foobar(obj: Obj) {
return obj.foo + obj.bar;
} class Obj {
public readonly foo: string;
public readonly bar: string;
// whatever constructor
foobar() {
return this.foo + this.bar;
}
} For me, there isn't much, except that with immutable pojos + functions I don't have to worry about |
@ianldgs There's should be no difference between them, it's just semantics. Anyway, consider my post to just be a big fat thumbs up for your post then! 🙂 |
Hm, actually, thinking back, the presence of the access modifier should already indicate it's not renaming
unless I'm also missing something |
@sandersn what part does need to be discarded. Maybe if we knew all that needed to be decided beforehand we could create a poll to vote on the best solution. I know it has not have been done here before, but it could provide great result :) |
This is my I workaround with default values for simple classes that don't have methods: class Attribute {
name!: string
value!: string
constructor(args?: Partial<Attribute>) {
Object.assign(this, { name: '', value: '' } as Attribute, args)
}
} And if you want optional-without-defaults properties: class Attribute {
name?: string
value?: string
constructor(args?: Attribute) {
Object.assign(this, args)
}
} |
Is using |
Still want this in 2023 |
Hello is there a roadmap for this feature? Would greatly improve the ergonomics of typescript! |
How about a syntax with a new keyword? The keyword This is an example of the possible syntax: class Test {
deconstructing constructor(
private readonly param1: string;
protected param2?: number;
public param3: string;
param4 = param3;
) {
// param2 is optional and yet can be followed by non-optional parameters
// param4 is local to the constructor only
}
} Some nuances:
The proposed syntax:
Downsides:
And this is the produced code/typescript class Test {
private readonly param1: string;
protected param2?: number;
public param3: string;
constructor(options: {
param1: string;
param2?: number;
public param3: string;
param4?: string
}) {
this.param1 = options.param1;
this.param2 = options.param2;
this.param3 = options.param3;
var param4 = options.param4 ?? param2;
// param4 is local to the constructor only
}
} |
I really like the solution proposed by @rodrigolive in #5326 (comment). Most of it can even be achieved already by adding branded types to their example (I'm sure this can be improved, suggestions are welcome): declare const tag: unique symbol;
type ConstructedProp<T> = T & { [tag]: typeof tag };
type ConstructedProps<Class> = Pick<
Class,
{
[Key in keyof Class]: Required<Class>[Key] extends ConstructedProp<unknown>
? Key
: never;
}[keyof Class]
>;
type ConstructorProps<Class> = {
[Key in keyof ConstructedProps<Class>]: Required<
ConstructedProps<Class>
>[Key] extends ConstructedProp<infer PropType>
? PropType
: never;
};
const constructProp = <T>(prop: T): ConstructedProp<T> => prop as ConstructedProp<T>; Then, class props can be defined like so: class FooBar {
foo: ConstructedProp<number> = constructProp(this.props.foo);
bar: ConstructedProp<number> = constructProp(this.props.bar * 7);
baz?: ConstructedProp<number> = constructProp(this.props.baz);
qaz?: number;
constructor(private readonly props: ConstructorProps<FooBar>) {}
}
const foobar = new FooBar({ foo: 3, bar: 5 }); Unfortunately, |
@Tomaszal This is a nice type-safe workaround until a proper solution is in place, but it would break for any generic class, where generic used to be defined by parameter in constructor. Also a big negative is that you cannot mimic defining class properties in constructor. This results in looking thought whole class declaration for any constructor props that might be present. Also private properties would not be properly found, since they are not visible to other function/types. And lastly class extending would break since the properties of parent would be requested in children too. |
My current workaround is to create a class decorator that will destructure the first constructor argument "automagically" /**
* Class decorator for automagically destructuring the constructor argument
*/
function destructure<T, R extends [...unknown[]]>(
Klass: Constructor<T, R]>,
_context?: unknown,
): Class<T, [Partial<T>]> {
const properties = Object.getOwnPropertyNames(
new Klass({}),
) as unknown as Array<keyof T>;
return class
extends // @ts-expect-error classes as variables nonsense
Klass
{
constructor(argument: Partial<T> = {}) {
const a = [];
for (const property of properties) {
a.push(argument[property]);
}
super(...a);
}
} as unknown as Class<T, [Partial<T>]>;
} With I can use parameter properties in the class like so: export @destructure class Foo {
constructor(
public foo: string,
public bar: number,
) {}
} But then I will be able to call the decorated constructor with an object: const foo = new Foo({bar: 1, foo: 'hi'}); |
@awlayton Having to If I have a singleton pattern like: export @destructure class Foo {
static instance: Foo;
constructor(
public foo: string,
public bar: number,
) {
Foo.instance ??= this;
}
} ...now |
Yes that is true @kibiz0r. My solution is only helpful for very specific cases. Language support for it would be much better. |
Worth noting that the A class like this will break if you're using the ES2022 syntax for class fields, which is the default for target ES2022 or higher, including ESNext. For this to work, you'd need to set class Foo {
constructor(private props: { foo: number }) {}
private foo = this.props.foo;
} |
We still want this in 2024 |
@a-tarasyuk Where is the discussion happening? I want to follow that thread. |
|
@a-tarasyuk The comment above is in reaction to your [comment in #44912]. (#44912 (comment)). My question is where the mentioned discussion is happening. This thread is almost dead yet there seems to be a lot of interest for this specific feature in the community. If you think that the other leading members should add to this discussion, please welcome them here, or we could even schedule some kind of "meeting" to discuss this. |
What are the blockers to this suggestion? How can we get Parameter Properties support with Named Arguments? |
Today, we can take advantage of parameter properties to reduce the boilerplate, e.g:
Since 1.5, we can also use destructuring, e.g:
I've tried in many ways to combine both features, but had no success. So:
The text was updated successfully, but these errors were encountered: