Skip to content

TS2: Intermediate

chapter 8: Functions

function overloading

  • JS does not support function overloading
  • when you declare 2 functions with the same name, the latest one will be used, even if the 2 functions declarations differs in signature.
  • TS also does not support function implementation overloading, but it will throw an error if you declared 2 functions with the same name regardless of the signature.
  • TS supports function types overloading means that you overload only the function signature.
// function overloading
function add(a: number, b: number): number; // 1st overload. signature only
function add(a: number, b: number, format): string; // 2nd overload. signature only
// actual function implementation
function add(a: number, b: number, format: boolean = false) {
    return format ? `${a} + ${b} = ${a + b}` : a + b;
}

const x1 = add(1, 2); // 3, follows 1st overload, x1 is number
const x2 = add(1, 2, true); // "1 + 2 = 3", follows 2nd overload, x2 is string
  • overloads replaces the function type definition as the types of the arguments changes

Assert functions

  • assert functions are functions that evaluate an expression and throw an error if the expression is false.
  • Assert functions are sometimes used as type guards in pure JavaScript, where the static types of TypeScript are not available.
// assert function
function check(expr: boolean, msg: string) {
    if (!expr) {
        throw new Error(msg);
    }
}

function add(a: number, b: number): number {
    check(a > 0 && b > 0, "a and b must be defined");
    return a + b;
}

const x = add(-1, 2); // x is still a number, but the check function throws an error
  • TS is not aware of the assert function, so it will not throw an error if the expression is false.
  • the problem is that TypeScript compiler cannot infer the effect of the assert function on types.
  • TS introduced the asserts expression keyword to force functions to be aware of the assert function.
// assert functions
function assert(value: boolean, message: string = "Assertion failed"): asserts value {
    if (!value) {
        throw new Error(message);
    }
}

function addWithAssert(a: number, b: number): number {
    assert(a > 0 && b > 0, "a and b must be positive");
    return a + b;
}

const x = addWithAssert(1, 2); // 3, x is number
const y = addWithAssert(1, -2); // Error
  • assert functions are useful when the get used to assert the type of an expression.
function assert(value: boolean, message: string = "Assertion failed"): asserts value is string {
    if (typeof value !== "string") {
        throw new Error(message);
    }
}

function formatName(firstName: string | null, lastName: string | null): string {
    assert(firstName, "firstName is required");
    assert(lastName, "lastName is required");
    // ... complier is aware now that both firstName and lastName are 100% strings
    // compiler will not complain that .upperCase() is not a function
    return `${firstName.toUpperCase()} ${lastName.toUpperCase()}`;
}

chapter 9: Using Arrays, Tuples, and Enums

Arrays

  • JS arrays are dynamically sized, and can contain any type of data.
  • TS asserts the size flexibility of JS arrays, but restrict its contents to a specific type.
  • initializing empty arrays
const a1 = []; // a1 is any[]
// ---- if strictNullChecks is true ----
const a2 = []; // a2 is never[]

Tuples

  • Tuples are fixed-size arrays, and can contain any type of data.
  • each element in the tuple can have a different type
  • tuples are a feature that is provided by the TypeScript compiler, they don’t exist in JS.
// tuples
const t1: [number, string] = [1, "hello"]; // t1 is tuple of 2 elements: [number, string]
const [n, s] = t1; // tuple destructuring
t1.push(3); // is valid, but not recommended since it changes the concept of a tuple
t1.forEach((v) => console.log(v)); // is valid, but not recommended, v is string | number

// ---- array of tuples ----
// array of tuples
const t2: [number, string][] = []; // t2 is array of tuples of 2 elements: [number, string]
t2.push([1, "a"]);
t2.push([2, "b", 3]); // error, type '[number, string, number]' is not assignable to type '[number, string]'.
  • tuples are standard arrays, they have no features specialized for them, it is the responsibility of the programmer to make sure the tuples are compatible with the array.
  • tuples can have optional elements
// tuples with optional elements
const t3: [number, string?] = [1];

const [n3] = t3; //valid,
const [_n3, _s3] = t3; //valid, _s3 is undefined

const t4: [number, string?] = [1, "a"];
const [n4, s4] = t4; //valid

t4.forEach((v) => console.log(v)); //valid, v is string | number | undefined

Enums

  • enums are a feature that is provided by the TypeScript compiler, they don’t exist in JS.
  • enums can work as value and type
enum Color {
    Red, // 0
    Blue, // 1
    Green, // 2
}

const printColor = (color: Color) /* enum used as type */ => {
    if (color === Color.Red /** enum used as value */) {
        console.log("Red");
    } else {
        console.log("Not red");
    }
};

const anotherColor: Color = 15; // valid, since enum values are numbers
  • ach enum value has a corresponding number value that is assigned automatically by the compiler and that starts at zero by default.
  • at runtime, enum values by default assigned a number type, but you can change this behavior by using the string key.
  • The compiler enforces type checking for enums, which means that you will receive an error if you try to compare values from different enums, even when they have the same underlying number value.
// compare different enums
enum Enum1 {
    A,
    B,
}

enum Enum2 {
    A,
    D,
}

const enum1: Enum1 = Enum1.A;
const enum2: Enum2 = Enum2.A;

if (enum1 === enum2) {
    // error, This condition will always return 'false' since the types 'Enum1' and 'Enum2' have no overlap.
    console.log("equal");
}
  • Enums provide an array-indexer style syntax that can be used to get the name of a value, like this:
// accessing enum values by index
enum Enum3 {
    A,
    B,
    C,
}

const enum3 = Enum3[2]; // C, gives the key name of the value at index 2, not the value itself
const enum4 = Enum3[3]; // undefined
  • By default, the TypeScript compiler starts assigning number values for an enum with zero and will compute the values by incrementing the previous value.
  • you can change the above behavior by providing different values for the first enum value.
// supply value for enum
enum Enum5 {
    A = 1,
    B, // 2, compiler will assign previous value + 1
    C = 28,
    D, // 29, compiler will assign previous value + 1
}

console.log(Enum5); // { '1': 'A', '2': 'B', '28': 'C', '29': 'D', A: 1, B: 2, C: 28, D: 29 }
console.log(Enum5.D); // 29
  • be careful when giving only some of the values for an enum, the compiler will assign the rest of the values automatically which may lead to duplicate values.
// issue when defining only few enum values
enum Enum6 {
    A = 1,
    B = 5, // 5, manually assigned
    C = 4,
    D, // 5, compiler will assign previous value + 1
}

console.log(Enum6); //{ '1': 'A', '4': 'C', '5': 'D', A: 1, B: 5, C: 4, D: 5 }

console.log(Enum6.D); // 5
console.log(Enum6.B); // 5
console.log(Enum6.B === Enum6.D); // true
  • to avoid duplicate values, if you assigned a value for one enum value, you must manually assign a value for all the other enum values.

Limitations of number enums

  • The compiler doesn’t prevent the assignment of a number to a variable whose type is an enum when the number doesn’t correspond to one of the enum values
  • if function returns an enum that uses numbers, it can return any number, not just the ones that are defined in the enum.
enum Enum7 {
    A, // 0
    B, // 1
    C, // 2
    D, // 3
}
const x: Enum7 = 14; // works, but not valid, since 14 is not one of the enum values

function f(): Enum7;
const y = f(); // y can be a number, and not an enum value 0-1-2-3
  • enums are implemented in JS as numbers, sp typeof can not distinguish between enums and numbers.

const enums

  • The TypeScript compiler creates an object that provides the implementation for an enum. this issue can be solved by using const enums.
  • to declare a const enum: const enum EnumName { A, B, C }
  • the difference is that normal enums got converted to JS objects, and const enums got converted to numbers or separate variables.
  • see how normal enums are converted to JS objects:

    // ts, normal enums
    enum Product {
        Hat,
        Gloves,
        Umbrella,
    }
    let productValue = Product.Hat;
    
    // converted to JS, normal enums
    var Product;
    (function (Product) {
        Product[(Product["Hat"] = 0)] = "Hat";
        Product[(Product["Gloves"] = 1)] = "Gloves";
        Product[(Product["Umbrella"] = 2)] = "Umbrella";
    })(Product || (Product = {}));
    let productValue = Product.Hat;
    
  • while const enums are converted to numbers or separate variables, notice the comment added by the compiler to indicate the enum key name:

    // ts, const enums
    const enum Product {
        Hat,
        Gloves,
        Umbrella,
    }
    let productValue = Product.Hat;
    
    // converted to JS, const enums
    let productValue = 0; /* Hat */
    
  • compiler has preserveConstEnums that forces the compiler to generate objects for const enums only during debugging of type declarations, and the objects of const enums will never go into the final JS code.

Literal Value Types

  • A literal value type specifies a specific set of values and allows only those values.
let x1: 1 | 2 | 3 = 1; // 1, 2, 3 are valid values
let x2: 1 | 10 | 100 = 1;

x1 = x2; // valid, 1 is in 1|2|3

x2 = 10;
x1 = x2; // invalid, 10 is not in 1|2|3
  • using template lateral string types
function getCityString(city: "London" | "Paris" | "Chicago"): `City: ${"London" | "Paris" | "Chicago"}` {
    return `City: ${city}` as `City: ${"London" | "Paris" | "Chicago"}`;
}
  • using type aliases
type City = "London" | "Paris" | "Chicago";
type CityString = `City: ${City}`;

function getCityString(city: City): CityString {
    return `City: ${city}` as CityString;
}