Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Introduction to TypeScript 2.1


May 07, 2021 TypeScript


Table of contents


Introduction to TypeScript 2.1

Keyof and Lookup types

In JavaScript, it is fairly common to use APIs with expected property names as parameters, but so far it has not been possible to express the type relationships that occur in these APIs.

Enter an index type query or keyof keyof T keyof T type is considered a sub-type of string.

Example

interface Person {
    name: string;
    age: number;
    location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string

The double property is the index access type, also known as the lookup type. Syntaxally, they look exactly like element access, but are written as types:

Example

type P1 = Person["name"];  // string
type P2 = Person["name" | "age"];  // string | number
type P3 = string["charAt"];  // (pos: number) => string
type P4 = string[]["push"];  // (...items: string[]) => number
type P5 = string[][0];  // string

You can use this pattern with the rest of the type system to get type-safe lookups.

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];  // Inferred type is T[K]
}

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
    obj[key] = value;
}

let x = { foo: 10, bar: "hello!" };

let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string

let oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"

setProperty(x, "foo", "string"); // Error!, string expected number

The type of map

A common task is to take the existing type and make each property completely optional. Let's say we have a Person:

interface Person {
    name: string;
    age: number;
    location: string;
}

Some versions of it are:

interface PartialPerson {
    name?: string;
    age?: number;
    location?: string;
}

With the Mapped type, PartialPerson can write broad transformations for theErson type, such as:

type Partial<T> = {
    [P in keyof T]?: T[P];
};

type PartialPerson = Partial<Person>;

Map types are generated by getting a collection of text types and calculating a set of properties for the new object type. They are similar to list in Python, but instead of generating new elements in the list, they generate new properties in the type.

In addition to this partial, Mapped Types can also express many useful transformations on a type:

// Keep types the same, but make each property to be read-only.
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// Same property names, but make the value a promise instead of a concrete one
type Deferred<T> = {
    [P in keyof T]: Promise<T[P]>;
};

// Wrap proxies around properties of T
type Proxify<T> = {
    [P in keyof T]: { get(): T[P]; set(v: T[P]): void }
};

Partial, Readonly, Record, and Pick

Partial and Readonly, as mentioned earlier, are very useful structures. You can use them to describe some common JS routines, such as:

function assign<T>(obj: T, props: Partial<T>): void;
function freeze<T>(obj: T): Readonly<T>;

Therefore, they are now included in the standard library by default.

We also include two other utility types: Record and Pick.

// From T pick a set of properties K
declare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;

const nameAndAgeOnly = pick(person, "name", "age");  // { name: string, age: number }
// For every properties K of type T, transform it to U
function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>

const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length);  // { foo: number, bar: number, baz: number }

Objects extend and rest

TypeScript 2.1 supports ESnext Spread and Rest.

Similar to array extensions, extended objects make it easy to obtain shallow copies:

let copy = { ...original };

Similarly, you can combine multiple different objects. In the following example, merged will have properties from foo, bar, and baz.

let merged = { ...foo, ...bar, ...baz };

You can also override existing properties and add new ones:

let obj = { x: 1, y: "string" };
var newObj = {...obj, z: 3, y: 4}; // { x: number, y: number, z: number }

Specifying the order of extension operations determines the final property in the generated object;

Object rest is a dual object of object extensions because they can extract any additional properties that are not picked up when deconstructing elements:

let obj = { x: 1, y: 1, z: 1 };
let { z, ...obj1 } = obj;
obj1; // {x: number, y:number};

Lower asynchronous functionality

This feature was supported prior to TypeScript 2.1, but only when positioning ES6/ES2015. TypeScript 2.1 provides functionality for the ES3 and ES5 runtimes, which means you're free to take advantage of it no matter what environment you're using.

Note: First, we need to make sure that our runtime has ECMAScript compatibility Promise that is globally available. T his may involve getting a polyfill for Promise, or relying on a run time that you might be targeting. We also need to make sure that TypeScript knows that Promise exists by setting the lib flag to something similar to "dom," "es2015" or "dom," "es2015.promise," "es5."

Example

tsconfig.json
{
    "compilerOptions": {
        "lib": ["dom", "es2015.promise", "es5"]
    }
}
dramaticWelcome.ts
function delay(milliseconds: number) {
    return new Promise<void>(resolve => {
        setTimeout(resolve, milliseconds);
    });
}

async function dramaticWelcome() {
    console.log("Hello");

    for (let i = 0; i < 3; i++) {
        await delay(500);
        console.log(".");
    }

    console.log("World!");
}

dramaticWelcome();

Compiling and running the output should result in correct behavior on the ES3/ES5 engine.

Support for external assistant libraries (tslib)

TypeScript injects auxiliary functions such as __extends for inheritance, __assign and JSX elements for extending operations in object text, and for asynchronous __awaiter.

There were two previous options:

  1. Inject helpers into each file that requires them, or
  2. No helper -- noEmit Helpers.

These two options still need to be improved; bundling the helper in each file is a pain point for customers trying to keep their packaging size smaller. And it doesn't include helpers, which means customers have to maintain their own helper library.

TypeScript 2.1 allows these files to be included in a separate module in the project, and the compiler will import them as needed.

First, install the tslib utility library:

npm install tslib

Second, compile the file using the --importHelpers command:

tsc --module commonjs --importHelpers a.ts

Therefore, given the following inputs, the resulting .js file will contain the helper imported into the tslib and used __assign instead of inline it.

export const o = { a: 1, name: "o" };
export const copy = { ...o };
"use strict";
var tslib_1 = require("tslib");
exports.o = { a: 1, name: "o" };
exports.copy = tslib_1.__assign({}, exports.o);

No type of import

Traditionally, TypeScript has been too strict on how to import modules. This is to avoid spelling errors and to prevent users from using modules incorrectly.

However, in many cases, you may only want to import existing modules that may not contain their own .d.ts files. I t was a mistake before. Starting with TypeScript 2.1, this is now easier.

With TypeScript 2.1, you can import JavaScript modules without a type declaration. If there is a type declaration (for example, declare module "foo" . . . or if node_modules/@types/foo) still has priority.

Imports of modules that do not declare files will still be marked as errors under --noImplicitAny.

Example

// Succeeds if `node_modules/asdf/index.js` exists
import { x } from "asdf";

Support -- target ES2016,-- target ES2017 and -- target ESNext

TypeScript 2.1 supports three new target values -- target ES2016,-- target ES2017 and target ESNext.

Using target --target ES2016 will instruct the compiler not to convert ES2016-specific features, such as the operator.

Similarly,-- target ES2017 will instruct the compiler not to convert ES2017-specific features such as async/await.

--target ESNext proposes features for the latest supported ES.

Improved any reasoning

Previously, if TypeScript could not determine the type of variable, the any type was selected.

let x;      // implicitly 'any'
let y = []; // implicitly 'any[]'

let z: any; // explicitly 'any'.

With TypeScript 2.1, rather than just selecting any, TypeScript infers the type based on what you last assigned.

This option is enabled only when setting -noImplicitAny.

Example

let x;

// You can still assign anything you want to 'x'.
x = () => 42;

// After that last assignment, TypeScript 2.1 knows that 'x' has type '() => number'.
let y = x();

// Thanks to that, it will now tell you that you can't add a number to a function!
console.log(x + y);
//          ~~~~~
// Error! Operator '+' cannot be applied to types '() => number' and 'number'.

// TypeScript still allows you to assign anything you want to 'x'.
x = "Hello world!";

// But now it also knows that 'x' is a 'string'!
x.toLowerCase();

The same type of tracking is now also made for empty arrays.

A variable that is declared as a variable with no type comments and an initial value of . . . is considered an implicit any. However, each subsequent x.push (value), x.unshift (value), or x[n? value operation evolves the type of variable based on the element added.

function f1() {
    let x = [];
    x.push(5);
    x[1] = "hello";
    x.unshift(true);
    return x;  // (string | number | boolean)[]
}

function f2() {
    let x = null;
    if (cond()) {
        x = [];
        while (cond()) {
            x.push("hello");
        }
    }
    return x;  // string[] | null
}

Any errors are implied

One of the great benefits of this is that you'll see fewer implicit any errors when you run noImplicitAny. Implicit any errors are reported only if the compiler cannot know the type of variable without type comments.

Example

function f3() {
    let x = [];  // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined.
    x.push(5);
    function g() {
        x;    // Error: Variable 'x' implicitly has an 'any[]' type.
    }
}

Better infer text types

Strings, numbers, and Boolean text types (such as "abc," 1, and true) are inferred only when explicit type comments exist. Starting with TypeScript 2.1, text types are always inferred for the const variable and the runonly property.

The type inferred for a const variable or readonly property that does not have a type comment is the type of the text initial value set item. T he type inferred for a let variable with an initial value seter and no type comments, var variables, parameters, or non-readonly properties is the extended text type of the initialization program. The widening type for string text types is stringing, number is for numeric text types, boolean is for true or false, and contains enumerations of text types.

Example

const c1 = 1;  // Type 1
const c2 = c1;  // Type 1
const c3 = "abc";  // Type "abc"
const c4 = true;  // Type true
const c5 = cond ? 1 : "abc";  // Type 1 | "abc"

let v1 = 1;  // Type number
let v2 = c2;  // Type number
let v3 = c3;  // Type string
let v4 = c4;  // Type boolean
let v5 = c5;  // Type number | string

Text type extensions can be controlled by explicit type comments. S pecifically, when an expression of a type of text is inferred for a const position without a type comment, the const variable infers a wider text type. However, when the const position has an explicit text type comment, the const variable gets a non-widening text type.

Example

const c1 = "hello";  // Widening type "hello"
let v1 = c1;  // Type string

const c2: "hello" = "hello";  // Type "hello"
let v2 = c2;  // Type "hello"

The return value for using the super call is 'this'

In ES2015, the constructor of the returned object implicitly replaces the this value with any caller of the super(). T herefore, it is necessary to capture any potential return value of super() and replace it with this. This change allows the use of Custom Elements, which uses this attribute to initialize browser-allocated elements with user-written constructors.

Example

class Base {
    x: number;
    constructor() {
        // return a new object other than `this`
        return {
            x: 1,
        };
    }
}

class Derived extends Base {
    constructor() {
        super();
        this.x = 2;
    }
}

Output:

var Derived = (function (_super) {
    __extends(Derived, _super);
    function Derived() {
        var _this = _super.call(this) || this;
        _this.x = 2;
        return _this;
    }
    return Derived;
}(Base));
This change causes the behavior of extended built-in classes (e.g., Error, Array, Map, etc.) to be interrupted.

Configuration inheritance

Typically, a project has multiple output targets, such as ES5 and ES2015, debugging and production, CommonJS, and System;

TypeScript 2.1 supports the use of extends inheritance configurations, where:

  • Extends is a new top-level property in tsconfig.json (including comfilerOptions, files, include, and exclude).
  • The value of the extends must be a string that contains the path to another profile to inherit.
  • The configuration in the base file is loaded first, and then overwritten by the configuration in the inheritance profile.
  • Loops between profiles are not allowed.
  • Files, include, and exclude override those in the base profile from the inheritance profile.
  • All relative paths found in the profile are resolved relative to the profile from which they originate.

Example

configs/base.json:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

tsconfig.json:

{
  "extends": "./configs/base",
  "files": [
    "main.ts",
    "supplemental.ts"
  ]
}

tsconfig.nostrictnull.json:

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "strictNullChecks": false
  }
}

New -- alwaysStrict

Reasons to call the compiler using --alwaysStrict:

  1. All code is parsed in strict mode.
  2. Write the instruction "use strict" on top of each generated file;

Modules are resolved automatically in strict mode. For non-module codes, a new flag is recommended.