Blog-Archiv

Montag, 7. Mai 2018

Using TypeScript Compiler Options

Compiler options serve to sharpen the checks a compiler makes. The TypeScript compiler provides many such options. For example, when you set "strictNullChecks": true, you will have to fix following code:

function displayHello(elementId: string, displayText: string) {
    const element = document.getElementById(elementId);
    element.innerHTML = "Hello"+displayText;
}

The compile error message is:

error TS2531: Object is possibly 'null'.

It's about element. You have to fix it to more robust code:

function displayHello(elementId: string, displayText: string) {
    const element = document.getElementById(elementId);
    if (element)
        element.innerHTML = "Hello"+displayText;
    else
        throw "Did not find id "+elementId;
}

In this Blog I refer to a very useful Blog of Meziantou. I won't discuss the compiler options one by one, instead I will list those that I applied, and then show how it affected my 3D-geometry TS source that I wrote in one of my recent Blogs. I won't repeat the whole source here, I will show just the critical parts that had to be fixed.

Where to Put Compiler Options

You can call the tsc compiler with a list of files to compile on command line, or you can call it without any argument.
In first case you can give options directly in the command line, e.g.

tsc --strict *.ts

As all options are false by default, you don't need to give true or false on the command line, simply naming the option sets it to true.

In second case tsc will search in current directory for a file named

tsconfig.json

If it doesn't find such a file, it will search all parent directories upwards. So best is to put tsconfig.json into the root directory of your project, and compile your project with a simple tsc command in that directory. Beside compiler-options you can also list the files to compile inside tsconfig.json.

Many more things can be inside tsconfig.json. There is even a schema for it, although not so easy to read.

List of Recommendable Options

Mind that this is not the setting for a professional project but just a minimal one to sharpen the compiler.

{
    "compilerOptions": {
        "module": "ES6",
        
        "noImplicitAny": true, 
        "noImplicitThis": true,
        "strictFunctionTypes": true,
        "strictPropertyInitialization": true,
        "alwaysStrict": true,

        "allowJs": true,
        "checkJs": true,
        "noImplicitReturns": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noFallthroughCasesInSwitch": true
    },

    "include": [
        "**/*.ts"
    ]
}

JSON is a JavaScript-like data format, introduced by Douglas Crockford (author of the famous book "JavaScript, the Good Parts"). It is used extensively in the web world for data-interchange and configuration.

Mind that you can not have comments inside JSON, neither "//" nor "/**/". Thus I will explain that JSON listing here:

  • The "compilerOptions" section is a JSON object. Every property inside is a compiler switch.
    The first group of switches could be replaced by a simple "strict": true statement, but for clarity I have listed them one by one.
    The second group is not contained in "strict".
    All the options listed here need to be configured explicitly, because their default value is false.

  • The "include" section is a JSON array. Every expression inside will be taken into the list of files to compile. Wildcards are allowed, thus I can simply refer to all files ending with .ts. The expression "**/" reads "including subdirectories of any depth".

A special case is strictNullChecks.

        "strictNullChecks": true, 

This sounds quite useful, but leads to a lot of maybe misleading default definitions for class properties. I had this active, but decided later to remove it, because it makes it impossible to use the JS language default undefined.

Following list of compiler-options fortunately already has the correct default and needs not to be set:

        "allowUnreachableCode": false,
        "allowUnusedLabels": false,
        "suppressImplicitAnyIndexErrors": false,
        "suppressExcessPropertyErrors": false,

But you should make sure that they were not changed explicitly to true!

Consequences

You may become desperate when you apply this to your project, and then see the list of errors. But it pays off to fix them all. Your code will be much better after.

Here is the list I got when I compiled my (incredibly simple) 3D-geometry TS sources with the options above.

interfaces/geometry/3D/BoxImpl.ts(7,14): error TS2420: Class 'BoxImpl' incorrectly implements interface 'Box'.
  Types of property 'distance' are incompatible.
    Type '(other?: Point3D | undefined) => number' is not assignable to type '{ (): number; (other: Point): number; }'.
interfaces/geometry/3D/BoxImpl.ts(31,28): error TS2345: Argument of type 'this' is not assignable to parameter of type 'Point3D'.
  Type 'BoxImpl' is not assignable to type 'Point3D'.
    Types of property 'distance' are incompatible.
      Type '(other?: Point3D | undefined) => number' is not assignable to type '{ (): number; (other: Point): number; }'.
interfaces/geometry/3D/BoxImpl.ts(34,9): error TS2322: Type 'BoxImpl' is not assignable to type 'Box'.
  Types of property 'distance' are incompatible.
    Type '(other?: Point3D | undefined) => number' is not assignable to type '{ (): number; (other: Point): number; }'.
interfaces/geometry/3D/Point3DImpl.ts(15,5): error TS2416: Property 'distance' in type 'Point3DImpl' is not assignable to the same property in base type 'Point3D'.
  Type '(other?: Point3D | undefined) => number' is not assignable to type '{ (): number; (other: Point): number; }'.
    Types of parameters 'other' and 'other' are incompatible.
      Type 'Point' is not assignable to type 'Point3D | undefined'.
        Type 'Point' is not assignable to type 'Point3D'.
          Property 'z' is missing in type 'Point'.
interfaces/geometry/3D/Point3DImpl.ts(17,13): error TS2322: Type 'Point3DImpl' is not assignable to type 'Point3D | undefined'.
  Type 'Point3DImpl' is not assignable to type 'Point3D'.
    Types of property 'distance' are incompatible.
      Type '(other?: Point3D | undefined) => number' is not assignable to type '{ (): number; (other: Point): number; }'.
interfaces/geometry/3D/Point3DImpl.ts(19,42): error TS2345: Argument of type 'Point3D | undefined' is not assignable to parameter of type 'Point'.
  Type 'undefined' is not assignable to type 'Point'.
interfaces/geometry/3D/Point3DImpl.ts(20,42): error TS2345: Argument of type 'Point3D | undefined' is not assignable to parameter of type 'Point'.
  Type 'undefined' is not assignable to type 'Point'.
interfaces/geometry/3D/Point3DImpl.ts(21,42): error TS2345: Argument of type 'Point3D | undefined' is not assignable to parameter of type 'Point3D'.
  Type 'undefined' is not assignable to type 'Point3D'.
interfaces/geometry/3D/Point3DImpl.ts(26,9): error TS2322: Type 'Point3DImpl' is not assignable to type 'Point3D'.

Impressive! So there seems to be something wrong in my inheritance hierarchy. I start from top, trying to fix the first error.

The BoxImpl.ts message "Types of property 'distance' are incompatible" tells me that function signatures contradict each other. I can not overload function distance(other: Point) in Point.ts with distance(other: Point3D). Same is with transform(other: Point).

So I will fix this like the following: Point3D will get the more specific function names distance3D() and transform3D().

Point3D.ts
import { Point } from "../Point.js";

export interface Point3D extends Point
{
    readonly z: number;
    distance3D(): number;
    distance3D(other: Point3D): number;
    transform3D(origin: Point3D): Point3D;
}
Point3DImpl.ts
import { Point3D } from "./Point3D.js";
import { PointImpl } from "../PointImpl.js";

export class Point3DImpl extends PointImpl implements Point3D
{
    ....
 
    distance3D(other?: Point3D): number    {
        .... // code not changed!
    }
    
    transform3D(origin: Point3D): Point3D    {
        .... // code not changed!
    }

    ....
}

That was it. After doing these changes the compiler did not report any more errors. So all the messages above were follower problems due to incompatible function overloading!

Conclusion

Sharpening compiler options may require a little patience. But a lot of tsc errors seem to be followers of others. Don't be afraid of big error lists. Strict typing is always better than sitting for hours on debugging hard to reproduce bugs.




Keine Kommentare: