'Test invalid function arguments with TypeScript and JestJS

I've beend creating a node application with typescript and I'm using jest to write unit tests.

My issue: I can't really write unit tests simulating invalid function argument types, because typescript won't compile. I mean it's nice that typescript realises that I try to put wrong data in those functions, but the way I understood ts so far is, it only works during compile time. There won't be any checks of argument types during runtime.

So I still have to test the correct behaviour of my IO depending functions with jest, right?

I thought I'll just write a xyz.spec.js that and leave all the interfaces and types behind for that specific test. But then I receive an error from jest regarding the import of modules. I guess because it's not a ts file.

Do I have to change my jest or ts setup to make it work?

Here is a screenshot of the test and the error:

enter image description here

here my package.json:

{
  "name": "state",
  "version": "0.0.0",
  "files": [
    "build"
  ],
  "main": "build/index",
  "types": "build/index",
  "scripts": {
    "clean": "rimraf build && rimraf coverage",
    "format": "prettier --write \"{src,__tests__}/**/*.ts\" --single-quote --trailing-comma es5",
    "lint": "tslint --force --format verbose \"src/**/*.ts\"",
    "prepublishOnly": "npm run build",
    "start": "node ./build/index.js",
    "prebuild": "npm run clean && npm run format && npm run lint && echo Using TypeScript && tsc --version",
    "build": "tsc --pretty",
    "build:watch": "nodemon --legacy-watch src/index.ts",
    "test": "jest --no-cache",
    "test:watch": "jest --no-cache --watch",
    "coverage": "jest --no-cache --coverage"
  },
  "dependencies": {
    "mongoose": "^5.6.0"
  },
  "devDependencies": {
    "@types/jest": "^24.0.13",
    "@types/mongoose": "^5.5.6",
    "@types/node": "^10.14.7",
    "coveralls": "^3.0.2",
    "jest": "^24.8.0",
    "nodemon": "^1.19.0",
    "prettier": "^1.14.3",
    "rimraf": "^2.6.2",
    "ts-jest": "^24.0.2",
    "ts-node": "^8.1.0",
    "tslint": "^5.11.0",
    "tslint-config-prettier": "^1.15.0",
    "typescript": "^3.1.1"
  },
  "engines": {
    "node": ">=10.0.0"
  },
  "jest": {
    "preset": "ts-jest"
  }
}

and here my tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "target": "es2015",
    "outDir": "./<%= buildpath %>",
    "removeComments": true,
    "inlineSourceMap": true,
    "inlineSources": true,
    "preserveConstEnums": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

Best regards fea



Solution 1:[1]

What it solved for me in the end was to allow js for typescript. So my new tsconfig.json looks like this:

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "target": "es2015",
    "outDir": "./build",
    "removeComments": true,
    "inlineSourceMap": true,
    "inlineSources": true,
    "preserveConstEnums": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "allowJs": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

Solution 2:[2]

You can use as any in the argument of your test. That way you can add the "wrong" type and test your code with it. Assuming you want to test the someFunction() function which expects some sort of Prop object, for instance

interface Prop {
    id: number;
    type: string;
}

function someFunction(props: Prop): void {
    // ... do some stuff
}

Then your test could look something like this:

it('should not accept the wrong argument type', () => {
    const props = { id: 123 };
    const testSomeFunction = () => {
        someFunction(props as any);
    };
    expect(testSomeFunction).toThrow();
});

You could also have a look at Type Guards, to do runtime type checks: http://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types

function isValidProp(prop: Prop): prop is Prop {
    const keys = ['id', 'type'];
    return Object.keys(prop).every(key => keys.includes(key));
}

function someFunction(props: Prop): void {
    if(!isValidProp(prop)){
        throw new Error('invalid prop!');
    }
    // ... do some stuff
}

Solution 3:[3]

This is what @ts-expect-error is for. It tells typescript to suppress the following error so that you can write tests that otherwise would not compile. Unlike @ts-ignore, it also generates an error if the following statement does not actually produce an error. Here is an example:

class NotANumberError extends Error {
  constructor(msg: string) {
    super(msg);
    this.name = 'NotANumberError';
  }
}

function acceptsNumber(x: number): void {
  if (typeof x !== 'number') {
    throw new NotANumberError(`expected number, received ${x}`);
  }
}

describe('acceptsNumber', () => {
  it('throws NotANumberError when called with string', () => {
    // @ts-expect-error test passing invalid string argument
    expect(() => acceptsNumber('hello')).toThrow(NotANumberError);
  });
});

The acceptsNumber('hello') would normally not compile, but that error gets suppressed by the @ts-expect-error comment above it, allowing us to run and test the code even when types mismatch.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 fea17e86
Solution 2
Solution 3 daz