Advanced TypeScript

— by Sixian Li

Example: querySelector

  • Better type hints, smoother developer experience
  • Better type safety, fewer run-time errors
  • Better type knowledge, calmer when facing type errors

Why we care

Define "advanced"?

Agenda

Fundamentals

  • What type is
  • `extends` keyword
  • Refresher on generics, `keyof`, indexed access type and mapped type

1.

Conditional Types

  • General form
  • Distributivity

2.

Real-world example

Live coding

3.

Fundamentals

Type is a set of values

Type

Type Values Size
boolean true, false 2
string "a", "b", "Ollie"...
"foo"

Type is a set of values

Type

Type Values Size
boolean true, false 2
string "a", "b", "Ollie"...
"foo" "foo" 1

Type is a set of values

Type

Type Values Size
boolean true, false 2
string "a", "b", "Ollie"...
"foo" "foo" 1
string[] ["1"], ["2", "Kickflip"]
"foo" | "bar" "foo", "bar" 2

Type is a set of values

Type

Type Values Size
boolean true, false 2
string "a", "b", "Ollie"...
"foo" "foo" 1
string[] ["1"], ["2", "Kickflip"]
"foo" | "bar" "foo", "bar" 2
never

Type is a set of values

Type

Type Values Size
boolean true, false 2
string "a", "b", "Ollie"...
"foo" "foo" 1
string[] ["1"], ["2", "Kickflip"]
"foo" | "bar" "foo", "bar" 2
never (no value, similar to ∅) 0

    A extends B

extends

A is assignable to B

 Every value in the set A is assignable to a              variable of type B

 B can be replaced with any value in the set A       without a problem

Generics

function identity(arg: number): number {
  return arg;
}

function identity(arg: any): any {
  return arg;
}

const a = identity(3.14) // a is any

Building blocks for creating types from types, preserving information in the type flow

Generics

function identity(arg: any): any {
  return arg;
}

let a = identity(3.14) // a is any

function identity<T>(arg: T): T {
  return arg;
}

let happyA = identity(6.28) // happyA is number

Building blocks for creating types from types, preserving information in the type flow

Generic Constraints

function loggingName<T>(arg: T): T {
	console.log(arg.name); // Property 'name' does not exist on type 'T'.
	return arg;
}

function loggingName<T extends {name: string}>(arg: T): T {
	console.log(arg.name); // Now the compiler knows 'name' definitely exists in T
	return arg;
}

loggingName(3.14) // Nope
loggingName({name: 'Foo'}) // Yes

You may sometimes want to write a generic function that works on a set of types where you have some knowledge about 

input: an object type

output: a string or numeric literal union of its keys

keyof

type Point = { x: number; y: number };
type P = keyof Point;
       = 'x' | 'y'

input: an object type and a property

output: type of that property in the object type

Indexed Access Types

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // number

// it can be a union
type I1 = Person["age" | "name"];
        = number | string
// combine with keyof
type I2 = Person[keyof Person];
        = Person['age' | 'name' | 'alive'];
        = number | string | boolean

Indexed Access Types

type Admins = 
  | {name: 'Alice'}
  | {name: 'Bob'}

type AdminName = Admins['name']
               = 'Alice' | 'Bob'

input: an object type and a property

output: type of that property in the object type

input: an object type

output: a new type where each property is transformed from old properties

Mapped Types

let newObj = {};
for (const property in o) {
  newObj[property] = someNewValue;
}

type MappedType<Type> = {
  [Property in keyof Type]: SomeNewType;
}

Mapped Types

type MappedType<Type> = {
  [Property in keyof Type]: SomeNewType;
}

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

type FeatureFlags = {
  darkMode: () => void;
  newUserProfile: () => void;
};
 
type FeatureOptions = OptionsFlags<FeatureFlags>;
type FeatureOptions = {
    darkMode: boolean;
    newUserProfile: boolean;
}

input: an object type

output: a new type where each property is transformed from old properties

Key remapping via as(TypeScript 4.1+)

Mapped Types

let newObj = {};
for (const property in o) {
  const newProperty = transform(property);
  newObj[newProperty] = someNewValue;
}

type MappedTypeWithNewProperties<Type> = {
    [Property in keyof Type as NewProperty]: SomeNewType
}
  • Transform the key
  • Eliminate `never` after transformation(just like filter!)

Key remapping via as(TypeScript 4.1+)

Mapped Types

  • Transform the key
type Foo<Type> = {
    [Property in keyof Type as 'foo']: boolean
};

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

type FooPerson = Foo<Person>;
type FooPerson = {
  foo: boolean
}

iterate

transform

Conditional Types

General form

  SomeType extends OtherType ? TrueType : FalseType;

When the type on the left of the extends is assignable to the one on the right, then you’ll get the “true” branch; otherwise you’ll get the “false” branch.

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string; // number

type Example2 = RegExp extends Animal ? number : string; // string

Distributivity

type ToArray<Type> = Type extends any ? Type[] : never;

type StrArrOrNumArr = ToArray<string | number>;
                    = ToArray<string> | ToArray<number>;
                    = string[] | number[];

When conditional types act on a generic type, they become distributive when given a union type.

(1+2) \times 3 = 1\times3 + 2\times3

A real-wolrd example

type ACTION_TYPES =
  | { type: 'setQueryString'; newValue?: string }
  | { type: 'setSortOptions'; newSortOptions: ITopicsSortOptions }
  | { type: 'toggleStatusOption'; option: LifecycleState };

// current usage
dispatch({type: 'setQueryString', newValue: 'search me'})
dispatch({type: 'setSortOptions', newSortOptions: {/* Some content */}})
dispatch({type: 'toggleStatusOption', option: {/* Some content */}})

// If we can do...

A real-wolrd example

type ACTION_TYPES =
  | { type: 'setQueryString'; newValue?: string }
  | { type: 'setSortOptions'; newSortOptions: ITopicsSortOptions }
  | { type: 'toggleStatusOption'; option: LifecycleState };

// current usage
dispatch({type: 'setQueryString', newValue: 'search me'})
dispatch({type: 'setSortOptions', newSortOptions: {/* Some content */}})
dispatch({type: 'toggleStatusOption', option: {/* Some content */}})

// If we can do...
dispatch('setQueryString', {newValue: 'search me'})
// we can even ignore the optional value
dispatch('setQueryString') 

A real-wolrd example

type ACTION_TYPES =
  | { type: 'setQueryString'; newValue?: string }
  | { type: 'setSortOptions'; newSortOptions: ITopicsSortOptions }
  | { type: 'toggleStatusOption'; option: LifecycleState };

// current usage
dispatch({type: 'setQueryString', newValue: 'search me'})
dispatch({type: 'setSortOptions', newSortOptions: {/* Some content */}})
dispatch({type: 'toggleStatusOption', option: {/* Some content */}})

// If we can do...
dispatch('setQueryString', {newValue: 'search me'})
// we can even ignore the optional value
dispatch('setQueryString') 

THANK YOU