0

Consider this class that is used as a data model in a Model-View-Controller scenario (I'm using TypeScript 3.5):

export class ViewSource {
    private viewName : string;
    private viewStruct : IViewStruct;
    private rows : any[];
    private rowIndex : number|null;

    constructor(viewName : string) {
        // Same as this.setViewName(viewName);
        this.viewName = viewName;
        this.viewStruct = api.meta.get_view_struct(viewName);
        if (!this.viewStruct) {
            throw new Error("Clould not load structure for view, name=" + (viewName));
        }
        this.rows = [];        
        this.rowIndex = null;
    }

    public setViewName = (viewName: string) => {
        this.viewName = viewName;
        this.viewStruct = api.meta.get_view_struct(viewName);
        if (!this.viewStruct) {
            throw new Error("Clould not load structure for view, name=" + (viewName));
        }
        this.rows = [];        
        this.rowIndex = null;
    }

    public getViewStruct = ():IViewStruct => { return this.viewStruct; }

    public getCellValue = (rowIndex: number, columnName: string) : any => {
        const row = this.rows[rowIndex] as any;
        return row[columnName];
    }

}

This is not a complete class, I only included a few methods to demonstrate the problem. ViewSource is a mutable object. It can be referenced from multiple parts of the application. (Please note that being a mutable object is a fact. This question is not about choosing a different data model that uses immutable objects.)

Whenever I want to change the state of a ViewSource object, I call its setViewName method. It does work, but it is also very clumsy. Every line of code in the constructor is repeated in the setViewName method.

Of course, it is not possible to use this constructor:

constructor(viewName : string) {
    this.setViewName(viewName);
}

because that results in TS2564 error:

Property 'viewStruct' has no initializer and is not definitely assigned in the constructor.ts(2564)

I do not want to ignore TS2564 errors in general. But I also do not want to repeat all attribute initializations. I have some other classes with even more properties (>10), and the corresponding code duplication looks ugly, and it is error prone. (I might forget that some things have to bee modified in two methods...)

So how can I avoid duplicating many lines of code?

3 Answers 3

2

I think the best method to avoid code duplication in this case would be to create a function that contains the initialization code, but instead of setting the value, it retunrs the value that need to be set.
Something like the following:

export class ViewSource {
    private viewName : string;
    private viewStruct : IViewStruct;
    private rows : any[];
    private rowIndex : number|null;

    constructor(viewName : string) {
        const {newViewName, newViewStruct, newRows, newRowIndex} = this.getNewValues(viewName);
        this.viewName = newViewName;
        this.newViewStruct = newViewStruct;
        // Rest of initialization goes here
    }

    public setViewName = (viewName: string) => {
        const {newViewName, newViewStruct, newRows, newRowIndex} = this.getNewValues(viewName);
        // Rest of initialization goes here
    }

    privat getNewValues = (viewName) => {
        const newViewName = viewName;
        const newViewStruct = api.meta.get_view_struct(viewName);
        if (!newViewStruct) {
            throw new Error("Clould not load structure for view, name=" + (viewName));
        }
        const newRows = [];        
        const newRowIndex = null;
        return {newViewName, newViewStruct, newRows, newRowIndex};
    }

}

This way the only thing you duplicate is setting the values, not calculating them, and if the values calculations will get more complicated you can simply expand the returned value.

Sign up to request clarification or add additional context in comments.

3 Comments

Yeah I was hoping for { ...this } = this.getNewValues(viewName), that might be a good extension to TypeScript syntax.
You could put all values in one object and set them like so: this.values = this.getNewValues()
Yes, but then I would have to use this.values.propName instead of this.propName everywhere.
2

A less complex approach than the accepted answer is to use the //@ts-ignore[1] comment above each member that is initialized elsewhere.

Consider this contrived example

class Foo {
    // @ts-ignore TS2564 - initialized in the init method
    a: number;

    // @ts-ignore TS2564 - initialized in the init method
    b: string;

    // @ts-ignore TS2564 - initialized in the init method
    c: number;
    
    constructor(a: number, b: string) {
        if(a === 0) {
            this.init(a,b,100);
        } else {
            this.init(a,b,4912);
        }           
    }

    private init(a: number, b: string, c: number): void {
        this.a = a;
        this.b = b;
        this.c = c;
    }
}

Since TypeScript 3.9 there exists the //@ts-expect-error[2] comment, but I think @ts-ignore is suitable.

[1] Suppress errors in .ts files
[2] TS expect errors comment

2 Comments

When the original question was asked, there was no Typescript 3.9. I prefer this over the accepted solution though. It is still not perfect. { ... this } = this.init() would be the best, but that is still not supported.
@nagylz indeed @ts-expect-error is very new (and not what I use in this case). As far as I can tell @ts-ignore has been around since Typescript 2.6. I agree some sort of built-in support for { ... this } = this.init(), rather than peppering code with ts-comments, would be the superior.
1

Since TypeScript 2.7 you can use the definite assignment assertion modifier which means adding an exclamation mark between the variable name and the colon:

private viewName!: string

This has the same effect as adding a // @ts-ignore TS2564 comment above it as @RamblinRose suggested.

1 Comment

But it is much more compact. I like this solution even better, than you! (The { ...this } = this.init() syntax would be better but it does not exist.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.