Effective typescript
Dan Vanderkam
Chapter 1
TypeScript is a superset of JavaScript. TypeScript adds a type system that models JavaScript’s runtime behavior and tries to spot code which will throw exceptions at runtime.
2 main options: noImplicitAny
and strictNullChecks
. Enable them in new projects and migrate to them in old.
Typescript has structural typing: “If it walks like a duck and talks like a duck…” It has advantages and downsides. “Your types are open”, so it’s possible to pass Vector3d into method for Vector2d. Iterating over objects can be tricky to type correctly because of open types.
The any
type effectively silences the type checker and TypeScript language services. It can mask real problems, harm developer experience, and undermine confidence in the type system. Avoid using it when you can!
Chapter 2
- Take advantage of the TypeScript language services by using an editor that can use them.
- Use your editor to build an intuition for how the type system works and how TypeScript infers types.
- Know how to jump into type declaration files to see how they model behavior.
The general rule is that the values in an intersection type contain the union of properties in each of its constituents.
// Union types. First or second
type Padding = string | number
// Union types with common fields
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
type Pet = Bird | Fish // have only layEggs in common.
// Discriminating Unions. Use literal types to narrow down the possible current type.
type NetworkState =
| NetworkLoadingState
| NetworkFailedState
| NetworkSuccessState;
function logger(state: NetworkState): string {
switch (state.state) {
case "loading":
return "Downloading...";
case "failed":
// The type must be NetworkFailedState here,
// so accessing the `code` field is safe
return `Error ${state.code} downloading`;
case "success":
return `Downloaded ${state.response.title} - ${state.response.summary}`;
}
}
// Intersection types. Props from both
interface ErrorHandling {
success: boolean;
error?: { message: string };
}
interface ArtworksData {
artworks: { title: string }[];
}
type ArtworksResponse = ArtworksData & ErrorHandling;
Type operations apply to the sets of values (the domain of the type), not to the properties in the interface. And remember that values with additional properties still belong to a type.
interface PersonSpan extends Person {
“Assignable to”, “subset of”, “subtype” - all about PersonSpan.
Tuple is a subset of a number.
A symbol in TypeScript exists in one of two spaces:
- Type space
- Value space instanceof - value space, tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object. typeof - mean different things in a type or value context. In a type context, typeof takes a value and returns its TypeScript type. In a value context, typeof is JavaScript’s runtime typeof operator. typeof always operates on values. You can’t apply it to types.
type T1 = typeof p; // Type is Person type T2 = typeof email;
// Type is (p: Person, subject: string, body: string) => Response const v1 = typeof p; // Value is "object"
const v2 = typeof email; // Value is "function"
const v1 = typeof p; // Value is "object"
const v2 = typeof email; // Value is "function"
Prefer Type Declaration to Type Assertions
const alice: Person = { name: 'Alice' }; // Type declaration
const bob = { name: 'Bob' } as Person; // Type assertion
As a suffix, !
is interpreted as an assertion that the value is non-null.
Type assertions have their limits: they don’t let you convert between arbitrary types. The general idea is that you can use a type assertion to convert between A and B if either is a subset of the other. Use type assertions and non-null assertions when you know something about types that TypeScript does not.
-
Avoid object wrapper types(Number,String, etc.) When you access a method like
charAt
on a string primitive, JavaScript wraps it in a String object, calls the method, and then throws the object away. -
“excess property checking”, or “strict object literal checking” - disallowing unknown properties specifically on object literals. Excess property checking is an effective way of catching typos and other mistakes in property names that would otherwise be allowed by the structural typing system. Be aware of the limits of excess property checking: introducing an intermediate variable will remove these checks.
Item 14
Mapped type
type TopNavState = {
[k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
// keyof - takes a type and gives you a union of the types of its keys:
/**
* Make partial
*/
type Par = {[k in keyof F]?: F[k]}
Use extends
to contstrain the parameters in a generic type.
interface Name { first: string; last: string; }
type DancingDuo<T extends Name> = [T, T];
// constraining K
// read "extends" as "subset of"
type Pick<T, K extends keyof T> = { [k in K]: T[k]
}; // OK
Item 15
Index signature. Prefer it only for dynamic data.
type S = {[k: string]: string}
There are almost always better alternative to index signature.
Item 16 Prefer Arrays, Tuples, and ArrayLike to number Index Signatures
Understand that arrays are objects, so their keys are strings, not numbers. number as an index signature is a purely TypeScript construct which is designed to help catch bugs. Prefer Array, tuple, or ArrayLike types to using number in an index signature yourself.
Item 17: Use readonly to Avoid Errors Associated with Mutation
An important caveat to readonly is that it is shallow. Use readonly to prevent errors with mutation and to find the places in your code where mutations occur
Item 18: Use Mapped Types to Keep Values in Sync
“Fail closed” and “Fail open” approaches.
const REQUIRES_UPDATE: {[k in keyof ScatterProps]: boolean} = {
- all keys from ScatterProps
should be present.
Useful when adding property to one object reflect on the logic of another.
Chapter 3 Type Inference
There is a difference between “statically typed” and “explicitly typed” because there is a type inference.
Item 19: Avoid Cluttering Your Code with Inferable Types
Consider using explicit annotations for object literals and function return types even when they can be inferred. This will help prevent implementation errors from surfacing in user code.
Item 20: Use Different Variables for Different Types
To avoid confusion, both for human readers and for the type checker, avoid reusing variables for differently typed values.
Item 21: Understand Type Widening
const mixed = ['x', 1];
Control widening using const
.
When you write as const
after a value, TypeScript will infer the narrowest possible type for it.
Item 22: Understand Type Narrowing
Using if/else
, instanceof
, property check ("a" in ab
), Array.isArray()
.
Another common way to help the type checker narrow your types is by putting an explicit “tag” on them:
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string; }
This pattern is known as tagged union or discriminated union.
User defined type guard:
function isInputElement(el: HTMLElement): el is HTMLInputElement
- help narrowing type.
function isDefined<T>(x: T | undefined): x is T { return x !== undefined;
}
Item 23: Create Objects All at Once
Spread operator is good at type recognition.
Chapter 4
Item 28: Prefer Types That Always Represent Valid States
Item 32: Prefer Unions of Interfaces to Interfaces of Unions
Item 33: Prefer More Precise Alternatives to String Types
Item 36
Reuse names from the domain of your problem where possible to increase the readability and level of abstraction of your code.