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.
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
- https://github.com/type-challenges/type-challenges
- https://github.com/g-plane/typed-query-selector
- https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
- https://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/
- https://effectivetypescript.com/2020/08/12/generics-golden-rule/