Typescript - Hur man tvingar en klassmetod att anropas? (Gör en metod obligatorisk)


Mål

Jag har en Logger-klass

class Logger {
  private logs = [];

  public log = (message: string) => {
    // append `message` to logs
  };

  public complete = () => {
    // log all `logs` to the console
  };
}

och jag vill göra complete-metoden obligatorisk.

Vilket betyder att complete måste anropas vid någon tidpunkt. (för att vara specifik bör det vara den sista metoden som kommer att anropas) Ett fel bör kastas om complete aldrig kommer att anropas.

Exempel

(1)

const l = new Logger();
l.log('start processing');
doSomething();
l.log('processing has been finished');
l.complete();

l.complete() kommer att anropas, allt är bra ✔️

(2)

const l = new Logger();
l.log('start processing');
doSomething();
l.log('processing has been finished');

`l.complete()` kommer aldrig att kallas, därför borde det finnas ett fel ✖️

(3)

const l = new Logger();
l.log('start processing');
doSomething();
l.log('processing has been finished');
if (someVariable) {
  l.complete();
}

l.complete() kanske inte anropas, vänligen kasta ett fel ✖️

(4)

const l = new Logger();
l.log('start processing');
doSomething();
l.log('processing has been finished');
if (someVariable) {
  l.complete();
} else {
  l.complete();
}

l.complete() kommer definitivt att kallas, allt är bra ✔️

(5)

const l = new Logger();
l.log('start processing');
await doSomething();
l.log('processing has been finished');
l.complete();

l.complete() kommer att kallas asynkront, allt är bra ✔️

Svar #1

Såvitt jag vet erbjuder inte kompilatorn en metod för att göra detta. Jag har inte sett en exakt kopia av denna funktion efterfrågas tidigare, men det påminner mig om en liknande (se microsoft/TypeScript#21388) som frågar efter att man ska kunna märka en klassmetod så att subklassers överskrivningar måste anropa överklassens metod. Detaljerna skiljer sig från din fråga, men båda ber kompilatorn att varna om en viss metod inte anropas under vissa omständigheter. Och såvitt jag vet finns det inget sätt att göra något av detta i TypeScript, åtminstone inte i TypeScript 3.8. Så svaret på din fråga som ställd är "detta är inte möjligt".

Kanske skulle ett annat tillvägagångssätt fungera? Istället för att kräva att användaren av dina Logger-instanser anropar complete() som den sista metoden, vad om du bara gav användaren en Logger-instans inuti en Promise-liknande callback, och du anropar complete() när callbacken är klar? Det är skillnaden mellan "här är din Logger. Var vänlig och anropa complete() när du är klar" och "berätta för mig vad du vill göra med din Logger, och sedan anropar jag complete() när det är klart". Här är en möjlig implementation:

class Logger {

    private constructor() { }
    private logs: string[] = [];    
    public log(message: string) {
        this.logs.push(message)
    };

    private complete() {
        this.logs.forEach(l => console.log(l));
    };

    public static invoke<R>(cb: (logger: Logger) => R): R {
        const logger = new Logger();
        const r = cb(logger);
        if (r instanceof Promise) {
            const t = r.then(x => (logger.complete(), x)) as any as R;
            return t;
        } else {
            logger.complete();
            return r;
        }
    }
}

Så både Logger-konstruktören och complete()-metoderna är privata. Det enda man kan göra med Logger är att anropa invoke() på den, vilket tar en callback. Det finns en liten komplikation med att hantera både synkrona och asynkrona callbacks, eftersom jag antar att du vill att synkrona callbacks ska kallas synkront. Men i båda fallen kallas callbacken med den privata Logger-instansen, och sedan anropas dess complete()-metod.

I dina exempel ovan är 2 och 3 (och till och med 4) nu omöjliga att formulera. complete()-metoden kommer att kallas oavsett vad. Exempel 1 ser ut så här:

Logger.invoke(l => {
    l.log('start SYNC processing');
    doSomething();
    l.log('SYNC processing has been finished');
})

och exempel 5 ser ut så här:

Logger.invoke(async l => {
    l.log('start ASYNC processing');
    await doSomething();
    l.log('ASYNC processing has been finished');
});

Du kan verifiera att båda fungerar (var försiktig med att använda The TypeScript Playground eftersom den asynkrona loggningen kanske inte visas i Loggrutan, men de bör visas i dina faktiska konsolloggar).

Playground-länk till kod

Okej, hoppas det hjälper; lycka till!