-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Less verbose class initialization #57367
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
Comments
@MartinJohns thank you! Those cover |
That sounds like either #36165 or #32082. It's unlikely you'd be able to omit the fields entirely though - otherwise if you wrote class A implements B {} then TS would have to include the fields from |
#32082 looks close, thank you. Re type-directed emit, I wouldn't think so. I'm suggesting that these become interface B {
name: string
}
class A implements B {
} And: interface B {
name: string
}
class A implements B {
name: string
} i.e. ok if strict null checks are off, but gets "name is not definitely assigned in the constructor" with strict null checks. That's where Object.assign complements it well: interface B {
name: string
}
class A implements B {
constructor(params: B) {
Object.assign(this, params)
}
} This would make the error go away but js output is unaffected. So maybe it's still worth having one issue for those two changes together even if each is somewhat covered by those linked duplicates. |
Yes, that's type-directed emit, because TS would have to emit the |
Hmm is that right? This playground shows no effect of a field that's never set. Typescript: class A {
name: string
} Output: class A {
} |
The difference appears when you enable |
Interesting, I had missed that, thanks for explaining. I agree that this is a non-starter if it allows interfaces to affect emit. But couldn't this still be pretty easily workable: interface B {
x: string
}
class A implements B {
} Emits class A {
} Or more realistically, since the above class is fairly pointless: interface B {
x: string
}
class A implements B {
constructor(params: B) {
Object.assign(this, params)
}
} Emits: class A {
constructor(params) {
Object.assign(this, params)
}
} This is already what typescript emits. The only thing new would be that this second example no longer has a compiler error ( Could that even be a useful change not just for the sake of verbosity - what if you have a specific reason to want to avoid the |
A quite silly workaround, but that does seem to work ok at compile time and runtime: export function AutoThisAssigner<T>(): new (params: T) => T
export function AutoThisAssigner<T, X extends new (...args: any[]) => any>(
C: X,
): new (params: T, ...args: ConstructorParameters<X>) => T & InstanceType<X>
export function AutoThisAssigner<T, X extends abstract new (...args: any[]) => any>(
C: X,
): new (params: T, ...args: ConstructorParameters<X>) => T & InstanceType<X>
export function AutoThisAssigner(C: any = Object) {
return class extends C {
constructor(params: any, ...args: any[]) {
super(...args)
Object.assign(this, params)
}
}
} Can be used like this (note, no explicit constructor, no duplicating field types, no line-by-line field setting, but you still get type safety): interface ColumnParams {
table: string
name: string
type: string
nullable: boolean
}
class Column extends AutoThisAssigner<ColumnParams>() {
quotedName() {
return JSON.stringify(this.name)
}
}
const column = new Column({
table: 'foo',
name: 'bar',
type: 'text',
nullable: false,
})
console.log(column.table, column.quotedName()) It works by abusing class extension to be a cross between interface GeneratedColumnParams {
expression: string
stored: boolean
}
class GeneratedColumn extends AutoThisAssigner<GeneratedColumnParams, typeof Column>(Column) {
validate() {
if (!this.expression.includes('foo bar')) {
throw new Error(`Invalid expression: ${this.expression}`)
}
}
}
const generatedColumn = new GeneratedColumn(
{
expression: 'foo bar',
stored: true,
},
{
table: 'foo',
name: 'bar',
type: 'text',
nullable: false,
},
)
generatedColumn.validate()
console.log(generatedColumn.table, generatedColumn.quotedName(), `is stored: ${generatedColumn.stored}`) Playground for anyone interested. @MartinJohns @RyanCavanaugh since this is marked as a duplicate, but the issues it duplicates are years old, would you recommend something like this if we want something like this functionality now? Or is there an existing community solution that does something like this? Or are there hidden pitfalls to the above do you think? |
@mmkal runtime functionality is explicitly out-of-scope. Your solution looks good to me |
@RyanCavanaugh I'm not proposing runtime functionality. Sorry if it was unclear - I'm proposing no changes to emit whatsoever. Just to make this no longer a compile error (i.e. catch up with js - what's emitted right now runs the way most people would expect, despite the compiler slapping your wrist): interface A {
x: 1
y: 2
}
class B implements A {
constructor(a: A) {
Object.assign(this, a)
}
} Emit stays the same, so no new runtime functionality is introduced. My previous comment was just a hack that allows you to simulate the convenience of this with current typescript. But it's just a hack, involves dynamically building a class etc. I'm not proposing that hack be incorporated into typescript. |
@mmkal is the feature proposal that |
Both. I think either one is useful, and perhaps covered by other tickets, but the two together allow a significant reduction in noise and likelihood of errors. I thought the use case might be clearer/more convincing when proposed together. Edit:
I'd phrase it as |
So when you implement an interface but are missing properties from the interface in your class declaration, would they be implicitly declared (but still require assignment)? interface ExampleLike {
interfaceMember: number;
}
class Example implements ExampleLike {
// Not needed, implicit via `implements` clause
//interfaceMember: number;
public constructor(init: ExampleLike){
// Satisfies assignment requirement
Object.assign(this, init);
}
} In this case I would expect |
This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
just don't use interface ExampleLike {
a: string;
b: number;
c?: Object
}
// merging interface with the class
interface Example extends ExampleLike{}
class Example {
// Not needed, implicit via `extends` clause
//a: string; b:number; //etc.
public constructor(init: ExampleLike){
// Satisfies assignment requirement
Object.assign(this, init);
}
}
const x = new Example({a:"hi", b:3})
console.log(x.a, x.b, x.c) |
π Search Terms
class verbose, class boilerplate
β Viability Checklist
β Suggestion
Initializing classes which take an options-bag constructor parameter, and copy properties of the options-bag into fields, is very boilerplatey. It'd be good if a) a class could say it implements an interface without having to explicitly redeclare all the fields and b) if it could use something like
Object.assign(this, options)
to set many fields in one go.π Motivating Example
Here's an example of a
Column
class, say representing a database table column:Note that each field in
ColumnParams
results in three lines of code, only one of each carries any real information/intent in terms of what the program is supposed to do. And in fact the constructor has two bugs in it:this.table = params.name
andthis.name = params.table
. This is a mistake that's easy to write and very hard to catch - the compiler won't catch it.this.type = params.type
. If the tsconfig hasstrictNullChecks: true
this will be caught, but plenty of projects don't have that (this example is from a port of a python library, which I hope to one day enable strictNullChecks on, but that will require diverging from the port, so won't happen immediately)Plus, every time we add a property to
ColumnParams
(say we addcolumn: string
), we risk regressing these bugs after fixing.It'd be great if we could do this:
That way:
There are cases where the above might not be wanted - say there are some properties of
ColumnParams
that shouldn't be assigned tothis
- but of course the old form would continue to work.π» Use Cases
The text was updated successfully, but these errors were encountered: