Shorn
Shorn

Reputation: 21426

Typescript - how do I manipulate immutable classes?

I want to use immutable classes instead of immutable interfaces, for two reasons:

This is the closest I've been able to come up with that mostly does what I want:

interface IData4 {
  readonly thing1: string;
  readonly thing2: string;
}

class Data4 implements IData4 {
  readonly thing1: string;
  readonly thing2: string;

  constructor(that: IData4, props?: Partial<IData4>){
    Object.assign(this, that);
    if( props ){
      Object.assign(this, props);
    }
  }

  doStuff(){
    return this.thing1 == this.thing2;
  }
}

// more verbose than a normal ctor parameter list, but I like it better anyway
// more readable, and it makes transposition errors less likely when all
// the params are the same type
// I write code to create instances rarely, reading and updating is more frequent
let data4 = new Data4({thing1: "t1", thing2: "t2"});

// GOOD!: error because of "otherThing"
// let other = new Data4({thing1: "t1", thing2: "t2", otherThing: 'blah'});

// GOOD!: error because missing "thing2"
// let other = new Data4({thing1: "t1"});

// this is the usual update case
let data4a = new Data4(data4, {thing2: "t2a"});
log.debug("data4a: " + JSON.stringify(data4a));

// GOOD!  error because of "otherThing"
// let other = new Data4(data4, {thing2: "t2b", otherThing: 'blah'});

// BAD!  want "otherThing" to cause error
// but I'm unlikely to use this construct, I wouldn't specify the spread 
// operator again because I already specified the thing to copy from
let bad2b = new Data4(data4, {...data4, thing2: "t2b", otherThing: 'blah'});

// BAD! want "otherThing" to cause error 
// easy to do accidentally because I'm used to using interfaces 
let bad2c = new Data4({...data4, thing2: "t2b", otherThing: 'blah'});

let iData: IData4 = {thing1: 't1', thing2: 't2'};
// BAD! want an error about "otherThing" 
let iData2 = {...iData, thing2: 't2a', otherThing: 'blah'};

// GOOD! error because of "otherThing"
// let other: IData4 = {thing1: 't1', thing2: 't2', otherThing: 'blah'};

// GOOD! error because lack of "thing2"
// let other: IData4 = {thing1: 't1'};

Problems with this construct:

So, is there a better way to do this?

Upvotes: 3

Views: 514

Answers (1)

Shorn
Shorn

Reputation: 21426

It's not great, but I did sort of find an answer to the question "is there some way to package up functions with interfaces"?

You can use "declaration merging" to make something that approximates an interface with functions:

interface Data7{
  readonly thing1: string;
  readonly thing2: string;
}

namespace Data7{
  export function areThingsEqualLength(value: Data7): boolean {
    return value.thing1.length == value.thing2.length;
  }
}

let data7: Data7 = {thing1: "t1", thing2: "t2"};
let data7a = {...data7, thing2: "t2a"};
log.debug("data7a: " + JSON.stringify(data7a));

log.debug("same length thing?: " + Data7.areThingsEqualLength(data7));

// GOOD!
// error: "'otherThing' does not exist in type"
// let other7a: Data7 = {thing1: "t1", thing2: "t2a", otherThing: 'blah'};

// GOOD!
// error: "Property 'thing1' is missing in type '{ thing2: string; }'."
// let other7b: Data7 = {thing2: 't2b'};

// BAD!
// want it to fail on 'otherThing' 
let data7b = {...data7, otherThing: 'blah'};

// VERY BAD!
// allows creation of an invalid object.
// The function call will explode, even though it's written correctly
let badData7 = {...data7, thing2: undefined};
// log.debug("same length thing?: " + Data7.areThingsEqualLength(badData7));

You can use an import to turn Data7.areThingsEqualLength(data7) into areThingsEqualLength(data7) if preferred.

But I've also realised that I might want to avoid using interfaces: with their accompanying spread operator, they allow construction of invalid objects (see the "VERY BAD!" comment).

So the title question remains (maybe this shouldn't be an answer, maybe it should be an edit to the question, but then it'd be really long).

Upvotes: 1

Related Questions