logoFEIQ

TypeScript

Variables and values

What are the types of the variable x and y? #

let x = "hi";
const y = "hi" 
Answer

x is of type string and y is "hi". This happens because const can't change so the only value possible is "hi". It's a literal type.

NaN as a number: Will it compile? #

let n = 5;
n = NaN;
Answer

Yes, NaN will compile. NaN is a number in JavaScript.

Literal type: Will this compile? #

let a = 30;
function func(num: 30) {} // 👀
func(a);
Answer

No, this won't compile. The function only accepts num where the value is 30.

Objects

Optional properties: Does it compile? #

type AB = {
  a?: number;
  b: number;
};

const x: AB = { b: 1 };
x.a = undefined;
x.a = 1;

interface CD {
  c?: number;
  d: number;
}

const y: CD = { d: 1 };
y.c = undefined;
y.c = 1;
Answer

Yes, the code above compiles.

An optional property (ending with ?) will accept the undefined value too. By default, before initializing a property it has the undefined value, so there's no need to initialize for instance a when assigning an object to the variable x.

Interfaces and types work the same way regarding optional properties.

Index signatures: Will it compile? #

type Boat = { size: number }
type Marina = { [k: string]: Boat }

let marina: Marina = {}
// Note property titanic was not defined
marina.titanic.size = 300; 
marina.mcBoat.size = 30;
Answer

Yes, it will compile.

But if you run the code, it will throw an error because titanic is not defined.

Arrays

Push vs assign: Does it show errors on line A or B? #

let tuple: [number, number] = [1, 2];
tuple.push(3); // A
tuple = [1, 2, 3]; // B
Answer

It shows errors on line B.

  • Line A: No errors. Because tuples are regular arrays at runtime.
  • Line B: Errors. Because the variable tuple only accepts tuples with 2 numbers.

Defining array types: Which lines don't compile? #

let arr1:string[] // A
let arr2:[string] // B
let arr3:Array<string> // C
Answer

Line B won't compile.

This is not a valid syntax to define arrays. Notice line C will show string[] type and not Array<string>.

What is the type of mixedArray? #

const mixedArray = [1, "Bob"]
Answer

The type is (number | string)[].

TypeScript can infer the possible types from the contents of the array.

Types

Types: Will they merge? #

type Boat = {
  color: string;
};

type Boat {
engine: boolean;
}

Answer

No, types don't merge. This causes a duplicate identifier error.

Type objects: Can they be partially created? #

type Boat = {
  color: string;
};

let b: Boat = {};
Answer

No, the Boat object needs to always conform to the Boat type. If you need to partially build an object, you can use the utility Partial and then use a type guard to make sure the object is completely built.

Union types

Union: Does it compile? #

type Result = Error | 42;
let r: Result = new Error("boom");
r = 42;
Answer

Yes, it compiles.

Union 👀: Does it compile? #

type Result = string | number;
let r = "banana"; // 👀
r = 42;
Answer

No, it doesn't compile. The danger of implicit typing is that sometimes you might forget to add them. This works:

type Result = string | number;
let r: Result = "banana"; // Notice r is explicitly typed with `Result`
r = 42;

Derived types: What is the type of a? #

type Res = ["error", Error] | ["ok", string];

function f(): Res {
  return Math.random() >= 0.5
    ? ["error", Error("boom")]
    : ["ok", "some message"];
}

const [a] = f();
Answer

The type of the variable a is "error" | "ok". TypeScript can calculate the type for you.

Intersection types and Interfaces

Intersecting types of objects: Does it compile? #

type T1 = {
  a: string;
};

type T2 = {
  b: number;
};

let c: T1 & T2;
c = { a: "a", b: 1 };
Answer

Yes, it compiles. Intersections create a type that is the combination of both types.

Intersecting primitive types: Does it compile? #

let c: string & null;
c = "hi";
Answer

No, it doesn't compile.

c is of type never because something cannot work as type string and type null. The types are in conflict.

Will the interfaces merge? #

interface Boat {
  color: string;
}

interface Boat {
  engine: boolean;
}
const b = { color: "white", engined: true };
Answer

Yes. The interfaces merge and there are no errors. The variable b complies with the interface Boat.

Does it compile? interface vs type #

type Boat = {
  color: string;
};

interface Boat {
  engine: boolean;
}
Answer

No, because there's a duplicate identifier Boat. And you can't merge interfaces with types.

Types vs Interfaces: Will line A and/or B compile? #

type AType = {
  prop: string;
}

type BType = {
  prop: number;
}

type CType = AType & BType; // A
type MyProp = CType['prop']

interface AInterface {
  prop: string;
}

interface BInterface {
  prop: number;
}

interface CInterface extends AInterface, BInterface { // B
}
Answer

Line A compiles, but line B doesn't compile.

Currently, extending interfaces catches more conflicts than type intersections. Read more about it here.

any, unknown, never

any, unknown: Will it compile? #

const x: unknown = "whatever";
const y: any = x;
const z: unknown = x;
Answer

Yes, it will compile.

It's possible to assign unknown to any and vice-versa.

any: Will it compile? #

let x:any;
x.f()
Answer

Yes.

any doesn't check the type.

unknown: Will it compile? #

let x:unknown;
x.f()
Answer

No.

Although you can put anything an unknown type, you need to cast it first.

Narrowing (guards)

Will the code compile? #

type HasName = { name: string }
type User = { name: string, age: number }

const x: User = { name: 'Jamie', age: 40 }
const y: HasName = x;
Answer

It will compile.

It's possible to assign a type to another type more restrictive.

Rounding number: Will it compile? #

function f(n: number | string) {
  n.toFixed(2);
}
Answer

No, it won't compile. The n must be narrowed to a single type. Like this:

function f(n: number | string) {
  if (typeof n === "number") n.toFixed(2)
}

Null or: What is the type n on line A? #

function f(n: string | string[] | null) {
  if (n && typeof n === "object") {
    console.log(n); // A
  }
}
Answer

The type of n is string[].

You have to check if n is truthy because typeof null is object. This means that if the variable is null or string[], it will have the type "object".

Equality narrowing: What's the type of x in line A? #

function f(x: string | number, y: string | object) {
  if (x === y) {
    x; // A
  }
}
Answer

The type of x is string.

If 2 variables are equal (by using ===), then both variables must be of the same type. In this case, the only possibility is to be string.

Loose equality narrowing: What's the type of x in line A? #

function f(x: boolean | null | undefined) {
  if (x == null) {
    x; // A
  }
}
Answer

The type of x is null | undefined.

It's possible to narrow types by using loose equality. If you loosely compare a value to null, it can be null or undefined like in the case above.

in narrowing: What's the type of x in line A? #

type Boat = { engine: string };
type Car = { color: string };

function f(x: Boat | Car) {
  if ("engine" in x) {
    x; // A
  }
}
Answer

The type of x is Boat.

It's possible to narrow types by using the in operator.

instanceof narrowing: What's the type of x in line A? #

class Boat {
  constructor(public name: string) {}
}

function f(x: Boat | string) {
  if (x instanceof Boat) {
    x; // A
  }
}
Answer

The type of x is Boat.

It's possible to narrow types by using the instanceof operator.

type guard: What is ??? to make the code work? #

type Boat = { name: string }

function isBoat(value: any): value ??? Boat {
    return value && "name" in value;
}
let maybeBoat: unknown

if(isBoat(maybeBoat)) {
    maybeBoat // A maybeBoat is Boat
}
Answer

??? is the keyword is, a type guard.

type guard assertion: What is the type of maybeBoat in line A? #

type Boat = { make: string; model: string };

function assertIsBoat(valueToTest: any): 
  asserts valueToTest is Boat {
  const cond =
    valueToTest &&
    typeof valueToTest === "object" &&
    "make" in valueToTest &&
    typeof valueToTest["make"] === "string" &&
    "model" in valueToTest &&
    typeof valueToTest["model"] === "string";

  if (!cond) throw new Error("Value is not a Boat");
}

let maybeBoat: unknown;
assertIsBoat(maybeBoat);
maybeBoat; // A
Answer

The type of maybeBoat is Boat. Although if you run the code, it will thrown an error because maybeBoat is udnefined.

type guard assertion: What is the type of car in line A? #

type Boat = { make: string; model: string };
type Car = { seats: number };

function assertIsBoat(valueToTest: any): 
  asserts valueToTest is Boat {
  const cond = valueToTest && 
    typeof valueToTest === "object";
  if (!valueToTest) throw new Error("Value is not a Boat");
}

let car: Car = { seats: 4 };
assertIsBoat(car);
car; // A
Answer

The type of car is Car & Boat.

Even when the assertion is not good (like in the code above) TypeScript will trust you.

Can you assign a less strict value to a variable? #

function g(n: number | null) {
  const num: number = n;
}
Answer

No, the code won't compile. Because you can't assign a value that is less strict. And in this case the value n accepts number or null while num only accepts numbers.

But you can assign a more strict value to a less strict variable:

function f(n: number) {
  const num: number | null = n;
}
 export default ({ children }) => <PageLayout>{children}</PageLayout>;

Type assertions: Will the code compile? #

type T = { a: 1 }
let t1 = {} as T; // A
let t2: T = {} // B
Answer

No, the code won't compile for line B.

There are some situations when using as that will make the code more permissive like in the example above. Learn more about type assertions.

Functions

interface function: Replace comment A so the code compiles #

interface TwoNumsFn {
  // A
}
const add: TwoNumsFn = (a, b) => a + b
Answer

You should put (a: number, b: number): number.

type function: Replace comment A so the code compiles #

type TwoNumsFn = // A

const add: TwoNumsFn = function (x, y) {
return x + y;
}

const sub: TwoNumsFn = (a, b) => a - b
Answer
(x: number, y: number) => number

Bottom values: null, undefined and void

Non-null assertion operator: Will the code compile? #

type Boat = {
  location?: { country: string }; // Notice it's optional
};

const b: Boat = {};
b.location!.country = "Luxembourg";
Answer

Yes, the code will compile.

But it will throw an error during runtime because b.location is undefined.

Definite Assignment Operator: Will the code compile? #

class C {
  x!: number;
}
Answer

The code will compile.

The x! will suppress any errors even if the class field was not initialized.

Can a function typed to return only undefined compile? #

  function f(n: number): undefined {      
    return undefined;
  }
Answer

Yes. If you explicitly return undefined.

What should you write instead of __ so that you can ignore the return value of a function? #

  function f():___ {
  }
Answer

The keyword void.

Does the code compile for a function that has no return values but has undefined as return type? #

function f(): undefined {
    console.log(1)
}

console.log(f()) // Logs "undefined"
Answer

No, the code won't compile.

Even though functions that don't have return statements return undefined, when we say the function is going to return undefined there needs to be somewhere an explicit undefined return.

What is the return type when nothing is returned and the function has no return type? #

function f() {}
const x = f()
Answer

It's void.

Classes

Will the class with Promise on constructor show errors? #

  class Boat {
    color: string
    engine: boolean
    constructor(color: string) {
      this.color = color
      Promise.resolve().then(()=> this.engine = true)
    }
  }
Answer

Yes, there is an error because engine is not initialized.

The constructor is initializing the engine field but not immediately. This means users of class Boat are not guaranteed to have the engine field initialized.

Does the abstract class compile? #

// abstract classes are like "half a class".
// In the case below, classes that extend Boat must implement the `engine` field.
abstract class Boat {
  public abstract engine: string
  color: "white"
}
class Titanic extends Boat {
  public engine: string | string[] = ["big engine", "small engine"]
}
Answer

No, it will not compile. The engine field has to be compatible with the abstract class.

Does the constrained abstract class work? #

abstract class Boat {
  public abstract color: string
}
class Titanic extends Boat {
  public color: "white" = "white" // Notice the literal type
}
Answer

Yes, it will work without errors.

You can constrain a type but you can't relax it. The color in Titanic can only be the string "white".

Does the class with public on constructor compile? #

type Boat = {
  color: string;
};

class Titanic implements Boat {
  constructor(public color: string) {}
}
Answer

Yes, it will compile. A new Boat object will have the field color.

The public syntax is shorthand for the typical object initialization. This is class property inference from constructors. The code above is the same as this:

type Boat = {
  color: string;
};

class Titanic implements Boat {
  color: string;
  constructor(color: string) {
    this.color = color;
  }
}

Will the class and interface merge? #

class Boat {
  color: string = "white";
}

export interface Boat {
  engine: string;
}

const p = new Boat();
p.color;
p.engine;
Answer

No, and it will not compile. If a part of a merged declaration is exported, all the parts must be exported.

To make it work, remove the export from the code above. It will also work if you add export to the class:

export class Boat {
  color: string = "white";
}

export interface Boat {
  engine: string;
}

const p = new Boat();
p.color;
p.engine;

Will the same private field compile? #

class Boat {
  private engine: number;
  constructor(engine: number) {
    this.engine = engine;
  }
}

class Titanic extends Boat {
  private engine: number;
  constructor(engine: number) {
    super(engine);
    this.engine = engine;
  }
}
Answer

No, it won't compile. "Types have separate declarations of a private property 'engine'"

This happens because private fields are public at runtime. So both fields can be accessed and that creates a conflict.

Will the same private field (#) compile? #

class Boat {
  #engine: number;
  constructor(engine: number) {
    this.#engine = engine;
  }
}

class Titanic extends Boat {
  #engine: number;
  constructor(engine: number) {
    super(engine);
    this.#engine = engine;
  }
}
Answer

Yes, this code will compile. Contrary to the private keyword, private hashed times (e.g. #engine) are private at runtime.

Property not initialized: Does it compile? #

  class Boat {
    color: string
    engine: boolean
    constructor(color: string) {
      this.color = color
    }
  }
Answer

No, this doesn't compile. Because the engine property has no initializer and it's not defined on the constructor.

Function calling this: Does it compile? #

type Boat = {
  engines: string[];
};

function findEngine(engine: string) {
  return this.engines.indexOf(engine) !== -1;
}
Answer

No, it won't compile.

There is an assumption that this has an engines field. But this could be anything, so we cannot assume that. We can fix it by defining the type of this:

  type Boat = {
    engines: string[];
  };

  function findEngine(this: Boat, engine: string) {
    return this.engines.indexOf(engine) !== -1;
  }

Access modifiers: Will A, B, C and D compile? #

class Boat {
    f1: number;
    public f2: number;
    protected f3: number;
    private f4: number;

    constructor(f1: number, f2: number, f3:number, f4: number) {
        this.f1 = f1;
        this.f2 = f2;
        this.f3 = f3;
        this.f4 = f4;
    }
}

const b = new Boat(1,1,1,1)
b.f1 // A
b.f2 // B
b.f3 // C
b.f4 // D
Answer

C and D won't compile. A compiles because public is the default access modifier.

Access modifiers: Will A, B, C and D compile? #

class Boat {
    constructor(f1: number, public f2: number, protected f3: number, private f4: number) { }
}

const b = new Boat(1, 1, 1, 1)
b.f1 // A
b.f2 // B
b.f3 // C
b.f4 // D
Answer

A, C and D won't compile. A won't compile because it's being parsed as a typical constructor parameter.

Access modifiers: Which lines won't compile? #

class Boat {
    constructor(public f1: number, protected f2: number, private f3: number) { }
}

class Titanic extends Boat {
    f() {
        this.f1; // A
        this.f2; // B
        this.f3; // C
    }
}

const t = new Titanic(1, 1, 1) 

t.f1 // D
t.f2 // E
t.f3 // F
Answer

The lines C, E and F.

The protected field is only accessible from within the subclasses of Boat.

readonly field: Will there be type errors on A or B? #

class Boat {
    readonly f1: number;
    constructor(f1: number) {
        this.f1 = f1; // A
    }
}

const b = new Boat(1)
b.f1 = 2 // B
Answer

There will be an error on B.

Readonly fields can be initialized on the constructor. Remember, there won't be any runtime check.

Are excess properties checked? #

  interface Boat {
    color: string;
  }

  function logBoat(mcBoat: Boat) {
    console.log(mcBoat);
  }

  logBoat({ color: "white", engine: false });
Answer

The code above won't compile.

Excess properties are checked for object literals. The reason is logBoat is receiving an object literal with a field (engine) that will never be used.

It would work if the variable could be used for something else. This works:

interface Boat {
  color: string;
}

function logBoat(mcBoat: Boat) {
  console.log(mcBoat);
}

const reusableVar = { color: "white", engine: false };

// This is valid because reusableVar can be used in some other context
logBoat(reusableVar);

Generics

Simple generic: What should you put on ___ to make the code work? #

function f___(arr: T[]): T {
    return arr[0];
}
const first = f([1, 2])
Answer

You should put <T>.

The function look like this:

  function f<T>(arr: T[]): T {
    return arr[0];
}

Generic array: Which word should we put instead of ___? #

const arr: ___<number> = []
arr.push(1)
Answer

The word to use is Array.

Consider that Array<some_type> is the same as some_type[].

Multiple generic types: What is the type of x? #

interface Small<T, U> {
    f1: T;
    f2: U;
}

function f<T, U>(x: T, y: U): Small<T, U> {
    return { f1: x, f2: y }
}

const x = f(1, "a")
Answer

The type of x is Small<number, string>.

Generic arrow functions: Which line won't produce compilation errors? #

const identity = <T>(val: T) => val // A
const identity = (val: T)<T> => val // B
identity(3) // returns 3
Answer

Line A.

Like in regular functions, the generic type definition comes before the parentheses for the parameters.

Narrowed generic: Will the code compile? #

const id = <T>(val: T) => val
id(1) // A
id<string>(1) // B
Answer

The code won't compile on line B.

Because you narrowed the type of the function (by passing <string>) you will get a compilation error warning you the argument of type 'number' is not assignable to parameter of type 'string'.

Generic Promise: Will the code compile? #

const sleep = <T>(val: T, ms: number) => {
    return new Promise<T>((res) => setTimeout(() => res(val), ms))
}

sleep<number>("hey", 2000).then((val) => console.log(val))
Answer

The code won't compile.

To make it compile, you'd have to change the sleep call to accept strings. Like so:

sleep<string>("hey", 2000)

Errors

Throwing errors: Will the code compile? #

function f(n:number): number | Error {
    if(n === 3) throw new Error("I don't like 3");
    return n;
}

function g(n:number): number {
    if(n === 3) throw new Error("I don't like 3");
    return n;
}
Answer

Yes.

Any function can thrown an error.

Catch error message: Will lines A and B compile? #

function getErrorMessage(error: unknown): string {
    return (error instanceof Error) ? error.message : String(error);
}

function f() {
    try {
        throw new Error('Oh no!')
    } catch (error) {
        console.log(error.message); // A
    }
}

function g() {
    try {
        throw new Error('Oh no!')
    } catch (error) {
        console.log(getErrorMessage(error))  // B       
    }
}

Answer

No, line A will not compile.

The type of error is unknown. Learn more from Kent.

Enums and string literals

Learn more:

Enum: What's the output? #

enum DirectionEnum {
  Up = "up",
  Down = "down",
}
const enumFn =(dir: DirectionEnum) => dir

enumFn(DirectionEnum.Up) // output
enumFn("up") // output
Answer

The result is a type error.

The second push cannot infer the type.

String literal: What's the output? #

type DirectionLiteral = "up" | "down"
const literalFn = (dir: DirectionLiteral) => dir

literalFn("up") // output
Answer

The output is "up".

In this case, "up" can be inferred as being of type "DirectionLiteral". There is no need to generate code as it happens with enums.

Narrowing Enums and string literals: What's the output? #

enum DirectionEnum {
  Up = "up",
  Down = "down",
}

type DirectionLiteral = "up" | "down"

const enumFn = (dir: DirectionEnum) => dir
const literalFn = (dir: DirectionLiteral) => dir
const b:string = "up" // notice it's a string

if (b === "up") {
  enumFn(b); // output
  literalFn(b); // output
}
Answer

The output is a type error on the first output.

You can't narrow types for an enum by comparing directly to a string.

const enum: What will be the JavaScript created on line A? #

const enum Directions { Up, Down }
const dirs = [
  Directions.Down,
  Directions.Down,
  Directions.Up
]

console.log(dirs)
Answer

[1, 1, 0].

When using const enum all the enum code is erased during compilation and replaced by its corresponding value.

number vs string: What's the log? #

enum DirNum { Up, Down }
enum DirStr { Up = "up", Down = "down" }

console.log(DirNum.Up)
console.log(DirStr.Up)
Answer

The log is 0 and up.

Configuration

d.ts files: What should you put instead of XXX to cause a type error on line A? #

// typings.d.ts
XXX module 'lodash' {
  export function random(a: number, b: number): number;
}

// index.ts
import { random } from 'lodash';
random('banana'); // A
Answer

declare.

To describe the shape of libraries not written in TypeScript, we need to declare the API that the library exposes. More info on so and official deep dive

Instead of defining the types yourself, you can use Definitely Typed

Will the code compile if strictNullChecks is set to false? #

// strictNullChecks = false
let x: boolean = null;
let y: boolean = undefined;
Answer

The code will compile.

null and undefined are considered a subtype of any type when --strictNullChecks is set to false. This means type T and T | undefined or T | null are considered synonymous.

Learn more

If you want to learn more, visit the TypeScript documentation or Mike North's courses.