Skip to content

Instantly share code, notes, and snippets.

@SoEasy
Created December 12, 2017 13:10
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save SoEasy/5df7b9768463c44804d2773d043fd2fd to your computer and use it in GitHub Desktop.
Save SoEasy/5df7b9768463c44804d2773d043fd2fd to your computer and use it in GitHub Desktop.
TypeScript contract-based decorators
type MethodDecorator = (target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) =>
TypedPropertyDescriptor | void;
type AssertFn = (...args: Array<any>) => void;
class TSContract {
private static isCustomContractInterruptionCb: boolean = false;
private static contractInterruptionCb(message: string): void {
console.warn(message);
}
static setupContractInterruptionCb(cb: (message: string) => void): void {
if (TSContract.isCustomContractInterruptionCb) {
console.warn('Custom contract interruption callback already setted, break');
return;
}
TSContract.contractInterruptionCb = cb;
TSContract.isCustomContractInterruptionCb = true;
}
static assert(expression: boolean, errorMessage: string): void {
if (!expression) {
TSContract.contractInterruptionCb(errorMessage);
}
}
static In(assertFn: AssertFn): MethodDecorator {
return function(target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor {
const originalValue = descriptor.value;
function newValue(...args) {
assertFn(...args);
return originalValue.apply(this, args);
}
descriptor.value = newValue;
return descriptor;
};
}
static Out(assertFn: AssertFn): MethodDecorator {
return function(target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor {
const originalValue = descriptor.value;
function newValue(...args) {
result = originalValue.apply(this, args);
assertFn(result);
return result;
}
descriptor.value = newValue;
return descriptor;
};
}
}
class ContractTest {
@TSContract.In(a => {
TSContract.assert(typeof a === 'number', 'Not a number!');
})
@TSContract.Out(retVal => {
TSContract.assert(!retVal, 'Wat?! Return?!');
})
doSome(a: number, withReturn: boolean = false): void {
console.log(a);
if (withReturn) {
return a;
}
}
}
TSContract.setupContractInterruptionCb((message: string) => {
console.error(message);
});
// cant setup another callback
TSContract.setupContractInterruptionCb((message: string) => {
console.error(message);
});
const ct = new ContractTest();
ct.doSome(12);
ct.doSome('12');
ct.doSome(12, true);
@ericbolikowski
Copy link

Hey @SoEasy, thanks for putting this out there. I was researching Design-by-Contract + TypeScript and came across this (and this).

I'm surprised there isn't more resources or libraries out there around JS/TS and DBC. For JS, babel-plugin-contracts looks like the best thing out there.

Do you know of any other good resources implementing this?

@SoEasy
Copy link
Author

SoEasy commented Oct 7, 2019

Hello @ericbolikowski!
I know one solution for Ruby, here is it: https://github.com/sclinede/blood_contracts
And I know that author of that solution want to make it for frontend world.

But what I think about contract-based pre/post conditions and them implementation - thats only the tip of the iceberg. What shall we doing next? How to process contract interruption? Only log message? Or break our business-logic? Throw exception? How to proceed that exception? What if we have very very deep callstack and somewhere in down contract interruption happens? How to notify user about that exception? And what if that exception is not critical? Should we skip some exceptions and raise other?

Too much questions :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment