mmap.page

Pitfalls of TypeScript

Excess Property Checking for Object Literal #

TypeScript uses structural typing (to work with typical JavaScript code).

function print_height(person: { height: number }): void {
    console.log(person.height)
}

const somebody = { height: 150, weight: 150 };
print_height(somebody);

However, TypeScript has excess property checking for object literal. Thus if we inline the somebody declaration above, TypeScript raises an error:

print_height({ height: 150, weight: 150 }); // Error!

However, partial overlap is still permitted:

type Point = {
    x: number;
    y: number;
}
type Label = {
    name: string;
}
const partialOverlap: Point | Label = {
    x: 0,
    y: 0,
    name: "origin"
}
function print_pl(pl: Point | Label) {
    console.log(pl)
}
print_pl({ x: 0, y: 0, name: "origin"})

But overlap with conflicting properties is not permitted:

type Point = {
    x: number;
    y: number;
}
type SPoint = {
    x: string;
    y: string;
}
const conflictingProperties: Point | SPoint = {x: 0, y: "0"}

Function Overloads #

Function type declaration supports overloading.

function plus(x: number, y: number): number;
function plus(x: string, y: string): string;
function plus(x: number, y: number): boolean; // see notes below
function plus(x: number | string, y: number | string): number | string | boolean { // this line is not overload
    if (typeof x == "number" && typeof y == "number") {
        return x + y;
    } else if (typeof x == "string" && typeof y == "string") {
        return `${x}${y}`; // equivalent to `x + y`.
    }
}

In the above example, the third overload will not be matched forever. Because TypeScripts looks at the overload list, proceeding with the first overload attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. And since intersection type of functions are defined as function overloads in TypeScript, this breaks the commutative rule of intersection function. Given two function types F and G, F & G and G & F are not equivalent.

Not surprisingly, it is difficult to check function overloads for type equality.

With TypeScript's conditional types, testing type equality is intuitive:

type EqEq<T, S> = [T] extends [S] ? ([S] extends [T] ? true : false) : false

But it cannot handle function overloads and function intersection:

type FunctionOverloadsEquality = EqEq<
    { (x: 0, y: null): void; (x: number, y: null): void },
    { (x: number, y: null): void; (x: 0, y: null): void }> // true

type F = (x: 0, y: null) => void
type G = (x: number, y: string) => void
type FunctionIntersectionEquality = EqEq<F & G, G & F> // true

To check function overloads' type equality, use this cleaver implementation by Matt McCutchen:

type EqEqEq<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

type FunctionOverloads = EqEqEq<
    { (x: 0, y: null): void; (x: number, y: null): void },
    { (x: number, y: null): void; (x: 0, y: null): void }> // false

However, it still cannot handle function intersection:

type FunctionIntersection = EqEqEq<F & G, G & F> // true

Besides, EqEq considers any to equal to any type except never. This makes sense, since unlike Any or Anything in other languages, TypeScript's any is not a top type. any is assignable to any type except never. EqEqEq is more strict and does not consider any to be identical to other type.

Literal String, Literal Number, But Unique Symbol #

TypeScript has literal strings and literal numbers, but there is no literal symbols. Instead, there is unique symbols (only allowed in const declarations).

In other words:

let no_unique_string_type: 'non sense but valid' = 'non sense but valid'
let no_literal_symbol: symbol = Symbol("cannot express Symbol(0) | Symbol(1)")