Skip to content

TS1: Basics

chapter 5: Using TypeScript Compiler

NPM

  • The node_modules folder is typically excluded from version control because it contains a large number of files and because packages can contain platform-specific components that don’t work when a project is checked out on a new machine.
  • using npm install on a different machine may generate different set of packages, due to packages versions expressed in ranges, to ensure consistency, we use package-lock.json file to store the dependencies versions.

tsconfig

  • top level fields:
{
    "compilerOptions": {},
    "include": [],
    "exclude": [],
    "files": [],
    "references": [],
    "compileOnSave": true
}
  • compiler options:
{
    "compilerOptions": {
        "target": "es2018",
        "outDir": "./dist",
        "rootDir": "./src",
        "noEmitOnError": true, // don't emit compiled files if there are errors
        "lib": [
            // tells the compiler about the runtime environment, which features will be available for the emitted js code.
            // compiler can not convert advanced features, eg. Map, Set, generators, async functions, etc.. it is your responsibility
            // to provide the correct js libs at runtime. it the wrong lib was provided during the runtime, the code will not work.
            "es2018",
            "dom",
            "esNext"
        ],
        // module also depends on the runtime environment, it does not depend on what environment you have while developing.
        // instead, what matter is the environment on the client that will execute the code.
        // commonjs is supported by node and all browsers by default.
        // file extensions are required by commonjs in import statements. which may cause issues since complier has to deal with .js/.ts files.
        "module": "commonjs", // commonjs, amd, system, umd, es2015, es2016, es2017, es2018, esnext
        // moduleResolution sets the resolution strategy for module names.
        // node: local node modules in the project folder, node_modules in the node_modules folder, and the built-in node modules.
        // classic: node_modules in the node_modules folder, and the built-in node modules.
        "moduleResolution": "node", // node, classic
        "allowJs": true, // allow javascript files to be compiled
        "allowSyntheticDefaultImports": true, // allow imports from modules with no default export
        "baseUrl": "./", // base url for module resolution
        "checkJs": true, // check javascript files for errors
        "declaration": true, // generate a .d.ts file
        "downlevelIteration": true, // enables support for iterators when targeting older versions of JavaScript.
        "emitDecoratorMetadata": true, // include decorator metadata in emitted js, used with the experimentalDecorators option.
        "esModuleInterop": true, // used with allowSyntheticDefaultImports, to allow non-ES6 imports from modules with no default exports.
        "experimentalDecorators": true, // enable experimental support for ES7 decorators.
        "forceConsistentCasingInFileNames": true, // force consistent casing in file names, regardless of the operating system.
        "importHelpers": true, // enable legacy import helpers.
        "isolatedModules": true, // compile each file as a separate module.
        "jsx": "react", // enable JSX support.
        "jsxFactory": "React.createElement", // the factory function to use when creating elements for JSX.
        "noEmit": true, // do not emit outputs.
        "noImplicitAny": true, // error on expressions and declarations with an implied 'any' type.
        "noImplicitReturns": true, // requires all paths in a function to return a result.
        "noUncheckedIndexedAccess": true, // disallow properties accessed via an index signature until checked against undefined values.
        "noUnusedParameters": true, // report errors on unused parameters.
        "paths": [], // specify locations used to resolve module dependencies.
        "resolveJsonModule": true, // allow json files to be imported as js modules.
        "skipLibCheck": true, // skip type checking of declaration files.
        "sourceMap": true, // generate a source map for each output js file.
        "strictNullChecks": true, //  prevents null and undefined from being accepted as values for other types.
        "strict": true, // enable strict mode.
        "suppressExcessPropertyErrors": true, // prevent errors for objects that define properties not in their type.
        "typeRoots": [], // specify locations where `declaration files (.d.ts)` can be found.
        "types": [] // specify type definitions to be used.
    }
}

tsconfig target

  • compilerOptions.target: specifies the target version of the ECMAScript standard, the emitted js files will be compatible with this version.
  • possible values:
value description
ES3 1999, 3rd edition, baseline for all browsers
ES5 2009, 5th edition, focuses on consistency
ES6 2015, 6th edition, added: classes, modules, arrow functions, promises
ES2015 2015, equivalent to ES6
ES2016 2016, 7th edition, added: includes method on arrays and strings, exponential operator
ES2017 2017, 8th edition, added: async/await, tools for objects
ES2018 2018, 9th edition, added: spread and rest operators
ES2019 2019, 10th edition, added: more array functions, error handling, JSON formatting
ES2020 2020, 11th edition, nullish operator, optional chaining, and loading modules dynamically
ESNext experimental, the latest version of the ECMAScript standard

chapter 7: Understanding TypeScript

  • JS is a dynamically typed language, which means that the type of a variable is not known at compile time.
  • TS is a statically typed language, which means that the type of a variable is known at compile time.

any

  • allows the use of dynamic types, so compiler won’t check the type of a variable at compile time.
  • any is given to all variables with no type specified, or when compiler can not infer the type, unless noImplicitAny is set to true.

union types

  • allows the use of multiple types, eg. string | number.
  • you can implicitly use any method that’s shared between all types, eg. toString().
  • in the example above string | number, you can use toString() on a string or a number. but you can’t use .charAt() on a string or a number.
  • to work around that you need to check the type of the variable, eg. if (typeof x === 'string') { x.charAt(0); }
let x: string | number = someMethod();

x.toString(); // works
x.charAt(0); // error

if (typeof x === "string") {
    x.charAt(0); // works
} else {
    // x  is now definitely a number
    x.toFixed(); // works
}

// -----------------
const numberX = x as number;
numberX.toFixed(); // works, `on your own risk`

// -----------------
const numberXX = Number(x);
numberXX.toFixed(); // works, `on your own risk` since numberXX may be a NaN

type assertions

  • type assertions are used to tell the compiler that you know what type you want the variable to be.
  • used in different ways:
    • const x = <string>y;
    • const x = y as string;
  • No type conversion is happening when asserting a type, it is your own responsibility to make sure the type is correct.
  • assertion from a union type must be one of the types in the union type.
  • compiler will throw an error if you try to assert a type that is not in the union type.
let x: string | number = someMethod();

const y = x as string; // works
const z = x as number; // works
const w = x as boolean; // error, boolean in not in the union type
const z = x as unknown as boolean; // works, work around on your own risk
const zz = x as any as number; // works, work around on your own risk

type guards

  • type guards are used to check if a variable is of a certain type.
  • for primitive types, typeof is used to check the type. eg. if (typeof x === 'string') { ... }
  • for custom types, it is your responsibility to distinguish between the types.like:
    • for oop and classes, if (x instanceof MyClass) { ... }
    • for literal objects, if(obj.uniqueProperty === 'someUniqueConventionalValue') { ... }

never

  • never is a type that can never be assigned to anything.
  • some situations where never is inferred:

    • const x = (() => { throw new Error('oops') })(); x will never gets a value, and the type is inferred to be never.
    • when a statement is behind a type guard that is always false, eg:

      const x = 0;
      if (typeof x === "string") {
          // x is never here
      }
      
      const y: string | number = someMethod();
      switch (typeof y) {
          case "string":
              break;
          case "number":
              break;
          default:
              // y is never here
              break;
      }
      
    • when initializing an array without typing it

      const x = []; // never[]
      const y: T[] = []; // T[]
      

unknown

  • safer alternative to any when you don’t know the type of the variable.
  • unknown value can be assigned only any or unknown type, unless a type assertion or type guard is used, in which the value gets a different type.

    const x: unknown = someMethod(); // x is of type unknown
    const x1: number = x; // error, unknown can not be assigned to number
    const y: any = x; // works, x can be assigned to any
    
    if (typeof x === "string") {
        console.log(x); // x is of type string here
    }
    
    let x2 = x; // x3 is of type unknown
    let x3 = x as string; // x4 is of type string
    

Nullable types

  • TS treats null and undefined as legal values for all types
  • TS ignores null and undefined when checking the type of a variable, unless strictNullChecks is set to true.

    const func = (x: number) => {
        if (x > 0) return x.toFixed();
        return null;
    };
    
    const x6 = func(5); // x6 is of type string (null is ignored)
    const x7 = func(0); // x7 is of type string (null is ignored)
    
    // ----------------- if strictNullChecks is true -----------------
    const x8 = func(5); // error, x8 is of type string | null
    const x9 = func(0); // error, x9 is of type string | null
    
  • ignoring nullable values can lead to runtime errors that is hard to detect during development.

  • strickNullChecks :
    • tells the compiler not to ignore null/undefined, and prevent assigning them to other types.
    • all values that are possibly null/undefined must be guarded before using them
  • guarding against null/undefined

    • typeof null === 'object', while typeof undefined === 'undefined'
    • using ?? operator to check if a value is null/undefined eg. x ?? 'default'
    • using !== null and !== undefined to check if a value is null/undefined eg. x !== null && x !== undefined
    • using falsy checks to check if a value is null/undefined eg. x || 'default', note: this may get triggered for other falsy values too.
    • using ! after a nullable value to assert that it does exist eg. x! or x!.toFixed()
    • using type assertions (casting) to ensure the value does exist eg. (x as number).toFixed()
    // examples on methods to remove null from a union type with nullable values
    const func = (x: number, format: boolean = false): string | number | null => {
        if (x > 0) {
            return format ? x.toFixed(2) : x;
        }
        return null;
    };
    
    //default
    const x1 = func(5); // x1 is of type string | number | null
    
    // type assertion
    const x2 = func(0)!; // number | string (assertion)
    
    // type guard
    const x3 = func(0);
    if (x3 !== null) {
        // x3 is of type number | string (type guard)
    }
    
    // Definite Assignment Assertion
    let x8!: string | number;
    ((p) => {
        x8 = p!;
    })(func(5));