Pesquisa de site

Typescript - como eu manipulo classes imutáveis?


Quero usar classes imutáveis em vez de interfaces imutáveis, por dois motivos:

  • para que eu possa agrupar algum código com eles (como o método doStuff)
  • para não permitir "propriedades extras" (veja o exemplo abaixo: em qualquer lugar onde seja válido definir otherThing, eu quero que isso cause um erro de compilação)

Esta é a coisa mais próxima que consegui criar que faz na maior parte o que eu quero:

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'};

Problemas com esta construção:

  • todo o constructo de Interface + Classe é difícil de manusear e duplicar as definições de propriedades é bobo (além de propenso a erros)
  • isso ainda me permite especificar propriedades extras se eu usar o operador "spread"

Então, existe uma maneira melhor de fazer isso?

  • Existe alguma maneira de empacotar funções com interfaces como eu quero?
  • Ou há um tipo melhor do que "Parcial" que eu deveria estar usando?

    • Existe alguma maneira de alcançar aproximadamente a mesma funcionalidade do Partial, mas sem permitir que propriedades extras sejam definidas?
  • Ou existe alguma forma em Typescript de eu poder usar construtores normais com listas de parâmetros para criar um novo objeto com uma propriedade alterada, mas sem ter que especificar todas as propriedades?

    • Especificamente, eu não quero estar fazendo: let data4a=new Data4(data4.thing1, "t2a"});

Resposta #1

Não é ótimo, mas eu meio que encontrei uma resposta para a pergunta "existe alguma maneira de empacotar funções com interfaces"?

Você pode usar "mesclagem de declarações" para criar algo que se aproxime de uma interface com funções:

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));

Você pode usar um import para transformar Data7.areThingsEqualLength(data7) em areThingsEqualLength(data7) se preferir.

Mas eu também percebi que talvez queira evitar o uso de interfaces: com seu operador spread acompanhado, elas permitem a construção de objetos inválidos (veja o comentário "MUITO RUIM!").

Então a pergunta do título permanece (talvez isso não devesse ser uma resposta, talvez devesse ser uma edição na pergunta, mas então seria realmente longa).