TypeScript

JavaScript, one of the world's most-used programming languages, has become the official language of the web. Developers use it to write cross-platform applications that can run on any platform and in any browser.

Although JavaScript is used to create cross-platform apps, it wasn't conceived for large apps involving thousands or even millions of lines of code. JavaScript lacks the features of more mature languages that power today's sophisticated applications. Integrated development editors (IDEs) can find it challenging to manage JavaScript and maintain these large code bases.

TypeScript addresses the limitations of JavaScript, doing so without compromising the critical value proposition of JavaScript: the ability to run your code anywhere and on every platform, browser, or host.

TypeScript is an open-source language that was developed by Microsoft. It is a superset of JavaScript, which means you can continue using the JavaScript skills you've already acquired and add specific features previously unavailable to you.

Typescript works by adding types on top of JavaScript. Types are a way to describe the shape of an object, providing better documentation and allowing TypeScript to validate that your code is working correctly.

It does this by providing extra syntax, which is compiled into JavaScript.

Why TypeScript?

The core feature of TypeScript is its type system. In TypeScript, you can identify the data type of a variable or parameter by using a type hint.

Through static type checking, TypeScript catches code issues early in development that JavaScript can't usually catch until the code is run in the javascript engine. Types also let you describe what your code is intended to do. If you're working on a team, a teammate who comes in after you can easily understand it too.

Types also power the intelligence and productivity benefits of development tools like IntelliSense. This is significantly potentiated by using inference.

Inference is the process of determining the type of a variable based on its usage. For example, if you declare a variable and initialize it with a string, TypeScript will infer that it is a string. So most of the time, you will write almost JavaScript code, but with the added benefit of type checking.

Gotchas

It is important to note that we never run TypeScript. We are always running JS code. TypeScript is compiled (or transpiled) into JS code since browsers (and node) do not understand TypeScript.

This means that types are not checked at runtime, only at compile time. So we must be careful with unexpected cases like APIs or user input where we can't be sure of the type.

Examples

These are some small examples of typescript. We recommend you to try them in the Typescript Playground.

Basic Types

// Boolean
let isDone: boolean = false;
// In this case, we can let typescript infer the type from the assingment. The following syntax is equivalent:
let isDoneInferred = false;

// Declaring a type
type SomeType = {
  example: string;
};

let example: SomeType = { example: 'example' };

// This will produce a compile error
let badExample: SomeType = { example: 123 };

// Interface are types that can be extended
interface SomeInterface {
  example: string;
}

interface SomeInterface2 extends SomeInterface {
  example2: string;
}

let example2: SomeInterface2 = { example: 'example', example2: 'example2' };

// Function
function add(a: number, b: number): number {
  return a + b;
}

// The return type is optional since it can be inferred
function addInferred(a: number, b: number) {
  return a + b;
}

// Arrow function
const addArrow = (a: number, b: number): number => {
  return a + b;
};

// The return type is optional since it can be inferred
const addArrowInferred = (a: number, b: number) => {
  return a + b;
};

Examples in React

interface Props {
  name: string;
}

// Here the return type is inferred to be a ReactElement
const Example = ({ name }: Props) => {
  return <div>{name}</div>;
};

// Same here
function Example({ name }: Props) {
  return <div>{name}</div>;
}

// If we want it to be explicit
const Example: React.FC<Props> = ({ name }) => {
  return <div>{name}</div>;
};

// or

function Example({ name }: Props): ReactElement {
  return <div>{name}</div>;
}

Resources

There are many more details of typescript. The ones specified above are only the basics. You can find more information in the following resources:

Exercises

These exercises focus on type-level programming — you write only types, no runtime code. The TypeScript compiler itself is the test runner: a correct solution compiles with zero errors; an incorrect one fails on the Expect lines.

How to validate

Copy any exercise block into a .ts file and open it in your editor. If you have the TypeScript language server running (VS Code does this automatically), red squiggles on the Expect lines mean the test cases are failing. When all squiggles are gone, your implementation is correct.

Utility types used in every exercise

// Expect<true> — fails to compile if the type argument is not `true`
type Expect<T extends true> = T;

// Equal<X, Y> — resolves to `true` only when X and Y are identical types
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

// Simplify<T> — flattens intersection types into a single object type
// e.g. { a: 1 } & { b: 2 } → { a: 1; b: 2 }
// Useful when your implementation returns an intersection that is structurally
// equivalent to the expected type but not identical at the type level.
type Simplify<T> = { [K in keyof T]: T[K] };

Each exercise starts with = any as a placeholder. Replace it with a real implementation until the compiler errors on the test cases disappear.


Basic

Exercise 1: MyPick

Reimplement the built-in Pick<T, K> utility type from scratch. MyPick<T, K> should construct a type by picking the set of properties K from T.

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

type MyPick<T, K extends keyof T> = any; // implement this

// Test cases (do not modify)
type cases = [
  Expect<Equal<MyPick<{ a: 1; b: 2; c: 3 }, 'a'>, { a: 1 }>>,
  Expect<Equal<MyPick<{ a: 1; b: 2; c: 3 }, 'a' | 'b'>, { a: 1; b: 2 }>>,
  Expect<Equal<MyPick<{ a: 1; b: 2; c: 3 }, 'b' | 'c'>, { b: 2; c: 3 }>>,
];

Useful docs: Mapped Types


Exercise 2: TupleToObject

Given a tuple of string literals (declared with as const), produce an object type whose keys and values are the tuple's elements.

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

type TupleToObject<T extends readonly string[]> = any; // implement this

// Test cases (do not modify)
const tuple = ['tesla', 'model 3', 'model X'] as const;

type cases = [
  Expect<Equal<
    TupleToObject<typeof tuple>,
    { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X' }
  >>,
];

Useful docs: Mapped Types, Indexed Access Types


Medium — Generics & Inference

Exercise 3: MyReturnType

Reimplement the built-in ReturnType<T> utility type. Given a function type, produce the type that the function returns. This exercise introduces the infer keyword.

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

type MyReturnType<T extends (...args: any[]) => any> = any; // implement this

// Test cases (do not modify)
type cases = [
  Expect<Equal<MyReturnType<() => string>, string>>,
  Expect<Equal<MyReturnType<() => 123>, 123>>,
  Expect<Equal<MyReturnType<() => boolean>, boolean>>,
  Expect<Equal<MyReturnType<(a: number, b: string) => { ok: true }>, { ok: true }>>,
];

Useful docs: Conditional Types, Inferring Within Conditional Types


Exercise 4: DeepPartial

Make every property in an object optional, recursively. Unlike the built-in Partial<T>, this must also descend into nested objects.

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

type DeepPartial<T> = any; // implement this

// Test cases (do not modify)
type cases = [
  Expect<Equal<DeepPartial<{ a: string }>, { a?: string }>>,
  Expect<Equal<
    DeepPartial<{ a: { b: { c: number } } }>,
    { a?: { b?: { c?: number } } }
  >>,
  // Primitives and arrays are left as-is
  Expect<Equal<DeepPartial<string>, string>>,
  Expect<Equal<DeepPartial<number[]>, number[]>>,
];

Useful docs: Mapped Types, Conditional Types, Recursive Conditional Types


Hard

Exercise 5: ParseQueryString

Parse a query string like "name=John&age=30" into { name: "John"; age: "30" }. Uses recursive template literal parsing and an accumulator pattern.

type Expect<T extends true> = T;
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;
type Simplify<T> = { [K in keyof T]: T[K] };

type ParseQueryString<T extends string> = any; // implement this

// Test cases (do not modify)
// Simplify<> is used here because recursive intersection-based implementations
// produce { a: "1" } & { b: "2" } which is equivalent to but not identical
// to { a: "1"; b: "2" } at the type level.
type cases = [
  Expect<Equal<Simplify<ParseQueryString<'name=John'>>, { name: 'John' }>>,
  Expect<Equal<Simplify<ParseQueryString<'name=John&age=30'>>, { name: 'John'; age: '30' }>>,
  Expect<Equal<Simplify<ParseQueryString<'a=1&b=2&c=3'>>, { a: '1'; b: '2'; c: '3' }>>,
  Expect<Equal<ParseQueryString<''>, {}>>,
];

Useful docs: Template Literal Types, Inferring Within Conditional Types, Recursive Conditional Types