Effective TypeScript
Table of Contents
- 1. Effective Typescript
- 1.1. Preface
- 1.2. Getting to Know TypeScript
- 1.2.1. Item 1: Understand the Relationship Between TypeScript and JavaScript
- 1.2.2. Item 2: Know Which TypeScript Options You're Using
- 1.2.3. Item 3: Understand That Code Generation Is Independent of Types
- 1.2.4. Item 4: Get Comfortable with Structural Typing
- 1.2.5. Item 5: Limit Use of the any Type
- 1.3. TypeScript's Type System
- 1.3.1. Item 6: Use Your Editor to Interrogate and Explore the Type System
- 1.3.2. Item 7: Think of Types as Sets of Values
- 1.3.3. Item 8: Know How to Tell Whether a Symbol Is in the Type Space or Value Space
- 1.3.4. Item 9: Prefer Type Declarations to Type Assertions
- 1.3.5. Item 10: Avoid Object Wrapper Types(String, Number, Boolean, Symbol, BigInt)
- 1.3.6. Item 11: Recognize the Limits of Excess Property Checking
- 1.3.7. Item 12: Apply Types to Entire Function Expressions When Possible
- 1.3.8. Item 13: Know the Differences Between type and interface
- 1.3.9. Item 14: Use Type Operations and Generics to Avoid Repeating Yourself
- 1.3.10. Item 15: Use Index Signatures for Dynamic Data
- 1.3.11. Item 16: Prefer Arrays, Tuples, and ArrayLike to number Index Signatures
- 1.3.12. Item 17: Use readonly to Avoid Errors Associated with Mutation
- 1.3.13. Item 18: Use Mapped Types to Keep Values in Sync
- 1.4. Type Inference
- 1.4.1. Item 19: Avoid Cluttering Your Code with Inferable Types
- 1.4.2. Item 20: Use Different Variables for Different Types
- 1.4.3. Item 21: Understand Type Widening
- 1.4.4. Item 22: Understand Type Narrowing
- 1.4.5. Item 23: Create Objects All at Once
- 1.4.6. Item 24: Be Consistent in Your Use of Aliases
- 1.4.7. Item 25: Use async Functions Instead of Callbacks for Asynchronous Code
- 1.4.8. Item 26: Understand How Context Is Used in Type Inference
- 1.4.9. Item 27: Use Functional Constructs and Libraries to Help Types Flow
- 1.5. Type Design
- 1.5.1. Item 28: Prefer Types That Always Represent Valid States
- 1.5.2. Item 29: Be Liberal in What You Accept and Strict in What You Produce
- 1.5.3. Item 30: Don't Repeat Type Information in Documentation
- 1.5.4. Item 31: Push Null Values to the Perimeter of Your Types
- 1.5.5. Item 32: Prefer Unions of Interfaces to Interfaces of Unions
- 1.5.6. Item 33: Prefer More Precise Alternatives to String Types
- 1.5.7. Item 34: Prefer Incomplete Types to Inaccurate Types
- 1.5.8. Item 35: Generate Types from APIs and Specs, Not Data
- 1.5.9. Item 36: Name Types Using the Language of Your Problem Domain
- 1.5.10. Item 37: Consider "Brands" for Nominal Typing
- 1.6. Working with
any
- 1.6.1. Item 38: Use the Narrowest Possible Scope for any Types
- 1.6.2. Item 39: Prefer More Precise Variants of any to Plain any
- 1.6.3. Item 40: Hide Unsafe Type Assertions in Well-Typed Functions
- 1.6.4. Item 41: Understand Evolving
any
- 1.6.5. Item 42: Use
unknown
Instead ofany
for Values with an Unknown Type - 1.6.6. Item 43: Prefer Type-Safe Approaches to Monkey Patching
- 1.6.7. Item 44: Track Your Type Coverage to Prevent Regressions in Type Safety
- 1.7. Types Declarations and @types
- 1.7.1. Item 45: Put TypeScript and @types in devDependencies
- 1.7.2. Item 46: Understand the Three Versions Involved in Type Declarations
- 1.7.3. Item 47: Export All Types That Appear in Public APIs
- 1.7.4. Item 48: Use TSDoc for API Comments
- 1.7.5. Item 49: Provide a Type for
this
in Callbacks - 1.7.6. item 50: Prefer Conditional Types to Overloaded Declarations
- 1.7.7. Item 51: Mirror Types to Sever Dependencies
- 1.7.8. Item 52: Be Aware of the Pitfalls of Testing Types
- 1.8. Writing and Running Your Code
- 1.9. Migrating to TypeScript
- 1.9.1. Item 58: Write Modern JavaScript
- 1.9.2. Item 59: Use @ts-check and JSDoc to Experiment with TypeScript
- 1.9.3. Item 60: Use allowJs to Mix TypeScript and JavaScript
- 1.9.4. Item 61: Convert Module by Module Up Your Dependency Graph
- 1.9.5. Item 62: Don't Consider Migration Complete Until You Enable noImplicitAny
- author
- Dan Vanderkam
1. Effective Typescript
1.1. Preface
Whereas a reference book will explain the five ways that a language lets you do X, an Effective book will tell you which of those five to use and why.
1.2. Getting to Know TypeScript
1.2.1. Item 1: Understand the Relationship Between TypeScript and JavaScript
TypeScript is a superset of JavaScript.
1.2.2. Item 2: Know Which TypeScript Options You're Using
1.2.3. Item 3: Understand That Code Generation Is Independent of Types
1.2.3.1. Code with Type Errors Can Produce Output
1.2.3.2. You Cannot Check TypeScript Types at Runtime
interface Square { width: number } interface Rectangle extends Square { height: number } type Shape = Square | Rectangle function calculateArea(shape: Shape) { if ('height' in shape) { shape; // Type is Rectangle return shape.width * shape.height } else { shape; // Type is Square return shape.width * shape.width } }
Another way would have been to introduct a "tag" to explicitly store the type in a way that's available at runtime:
interface Square { kind: 'square' width: number } interface Rectangle { kind: 'rectangle' height: number width: number } type Shape = Square | Rectangle function calculateArea(shape: Shpae) { if (shape.kind === 'rectangle') { shape // Type is Rectangle return shape.width * shape.height } else { shape // Type is Square return shape.width * shape.width } }
1.2.3.3. You Cannot Overload a Function Based on TypeScript Types
You can provide multiple declarations for a function, but only a single implementation:
function add(a: number, b: number): number function add(a: string, b: string): string function add(a, b) { return a + b } const three = add(1, 2) // Type is number const twelve = add('1', '2') // Type is string
1.2.3.4. Things to Remember
- Code generation is independent of the type system. This means that TypeScript types cannot affect the runtime behavior or performance of your code.
- It is possible for a program with type errors to produce code.
- TypeScript types are not available at runtime. To query a type at runtime, you need some way to reconstruct it. Tagged unions and property checking are common ways to do this. Some constructs, such as class, introduce both a TypeScript type and a value that is available at runtime.
1.2.4. Item 4: Get Comfortable with Structural Typing
- Understand that JavaScript is duck typed and TypeScript uses structural typing to model this: values assignable to your interfaces might have properties beyond those explicitly listed in your type definitions. Types are not "sealed".
- Be aware that classes also follow structural typing rules. You may not have an instance of the class you expect!
- Use structural typing to facilitate unit testing.
1.2.5. Item 5: Limit Use of the any Type
1.3. TypeScript's Type System
1.3.1. Item 6: Use Your Editor to Interrogate and Explore the Type System
1.3.2. Item 7: Think of Types as Sets of Values
- Think of types as sets of values (the type's domain). These sets can either be finite (e.g., boolean or literal types) or infinite (e.g., number or string).
- TypeScript types form intersecting sets rather than a strict hierarchy. Two types can overlap without either being a subtype of the other.
- Remember that an objet can still belong to a type even if it has additional properties that were not mentioned in the type declaration.
- Type operations apply to a set's domain. The intersection of A and B is the intersection of A's domain and B's domain. For object types, this means that values in A & B have the properties of both A and B.
- Think of "extends", "assignable to", and "subtype of" as synonyms for "sybset of".
1.3.3. Item 8: Know How to Tell Whether a Symbol Is in the Type Space or Value Space
- Every value has a type, but types do not have values. Constructs such as
type
andinterface
exist only in the type space. typeof
,this
and many other operators and keywords have different meanings in type space and value space.- Some constructs such as
class
orenum
introduce both a type and a value.
1.3.4. Item 9: Prefer Type Declarations to Type Assertions
- Prefer type declarations(: Type) to type assertions(as Type).
- Use type assertions and non-null assertions when you know something about types that TypeScript does not.
1.3.5. Item 10: Avoid Object Wrapper Types(String, Number, Boolean, Symbol, BigInt)
1.3.6. Item 11: Recognize the Limits of Excess Property Checking
index signature
interface Options { darkMode?: boolean [otherOptions: string]: unknown }
When you assign an object literal to a variable or pass it as an argument to a function, it undergoes excess property checking.
1.3.7. Item 12: Apply Types to Entire Function Expressions When Possible
- Consider applying type annotations to entire function expressions, ranther than to their parameters and return type.
- If you're writing the same type signature repeatedly, factor out a function type or look for an existing one. If you're a library author, provider types for common callbacks.
- Use
typeof fn
to match the signature of another function.
1.3.8. Item 13: Know the Differences Between type and interface
- Understand the differences and similarities between
type
andinterface
. - Know how to write the same types using either syntax.
- In deciding which to use in your project, consider the established style and whether augmentation might be beneficial.
1.3.9. Item 14: Use Type Operations and Generics to Avoid Repeating Yourself
type State = { userId: string pageTitle: string recentFiles: string[] pageContnets: string } type TopNavState = { [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k] }
Pick
's declaration in the standard library:
type Pick<T, K> = { [k in K]: T[k] }
Similar:
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>
- The DRY (don't repeat yourself) principle applies to types as much as it applies to logic.
- Name types rather than repeating them. Use
extends
to avoid repeating fields in interfaces. - Build an understanding of the tools provided by TypeScript to map between types. These include
keyof
,typeof
, indexing, and mapped types. - Generic types are the equivalent of functions for types. Use them to map between types instead of repeating types. Use
extends
to constrain generic types. - Familiarize yourself with generic types defined in the standard library such as
Pick
,Partial
, andReturnType
.
1.3.10. Item 15: Use Index Signatures for Dynamic Data
type Vec3D = Record<'x' | 'y' | 'z', number> // Type Vec3D = { // x: number // y: number // z: number //} type V3D = { [k in 'x' | 'y' | 'z']: number } // same as above type ABC = { [k in 'a' | 'b' | 'c']: k extends 'b' ? string : number } // Type ABC = { // a: number // b: string // c: number //}
- Use index signatures when the properties of an object cannot be known until runtime–for example, if you're loading them from a CSV file.
- Consider adding
undefined
to the value type of an index signature for safer access. - Prefer more precise types to index signatures when possible:
interfaces
,Records
, or mapped types.
1.3.11. Item 16: Prefer Arrays, Tuples, and ArrayLike to number Index Signatures
1.3.12. Item 17: Use readonly to Avoid Errors Associated with Mutation
- If your function does not modify its parameters then declare them
readonly
. This makes its contract clearer and prevents inadvertent mutations in its implementation. - Use
readonly
to prevent errors with mutation and to find the palces in your code where mutations occur. - Understand the defference between
const
andreadonly
. - Understand that
readonly
is shallow.
1.3.13. Item 18: Use Mapped Types to Keep Values in Sync
const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = { xs: true, ys: true, // ... } const PROPS_REQUIRING_UPDATE: (keyof ScatterProps)[] = [ 'xs', 'ys', // ... ]
Mapped types are ideal if you want one object to have exactly the same properties as another.
- Use mapped types to keep related values and types synchronized.
- Consider using mapped types to force choices when adding new properties to an interface.
1.4. Type Inference
1.4.1. Item 19: Avoid Cluttering Your Code with Inferable Types
- Avoid writing type annotations when TypeScript can infer the same type.
- Ideally your code has type annotations in function/method signatures but not on local variables in their bodies.
- 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.
1.4.2. Item 20: Use Different Variables for Different Types
1.4.3. Item 21: Understand Type Widening
1.4.4. Item 22: Understand Type Narrowing
function isInputElement(el: HTMLElement): el is HTMLInputElement { return 'value' in el } function isDefined<T>(x: T | undefined): x is T { return x !== undefined }
The el is HTMLInputElement
as a return type tells the type checker that it can narrow the type of the parameter if the function returns true
.
- Understand how TypeScript narrows types based on conditionals and other types of control flow.
- Use tagged/discriminated unions and user-defined type guards to help the process of narrowing.
1.4.5. Item 23: Create Objects All at Once
declare let hasMiddle: boolean const firstLast = { first: 'Harry', last: 'Truman' } const president = { ...firstLast, ...(hasMiddle ? { middle: 'S' } : {}) }
function addOptional<T extends object, U extends object>( a: T, b: U | null ): T & Partial<U> { return { ...a, ...b } } declare let hasMiddle: boolean const firstLast = { first: 'Harry', last: 'Truman' } const president = addOptional(firstLast, hasMiddle ? { middle: 'S' } : null)
- Prefer to build objects all at once rather than piecemeal. Use object spread
({ ...a, ...b })
to add propertties in a type-safe way. - Know how to conditionally add properties to an object.
1.4.6. Item 24: Be Consistent in Your Use of Aliases
- Aliasing can prevent TypeScript from narrowing types. If you create an alias for a variable, use it consistently.
- Be aware of how function calls can invalidate type refinements on properties. Trust refinements on local variables more than on properties.
1.4.7. Item 25: Use async Functions Instead of Callbacks for Asynchronous Code
async function fetchAll() { const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]) } function timeout(ms: number): Promise<never> { return new Promise((_resolve, reject) => { setTimeout(() => reject('timeout'), ms) }) } async function fetchWithTimeout(url: string, ms: number) { return Promise.race([fetch(url), timeout(ms)]) }
- Prefer
async
andawait
to row Promises when possible. They produce more concise, straightforward code and eliminate whole classes of errors. - If a function returns a Promise, declare it
async
.
1.4.8. Item 26: Understand How Context Is Used in Type Inference
1.4.9. Item 27: Use Functional Constructs and Libraries to Help Types Flow
- Use built-in functional constructs and those in utility libraries like Lodash instead of hand-rolled constructs to improve type flow, increase legibility, and reduce the need for explicit type annotations.
1.5. Type Design
1.5.1. Item 28: Prefer Types That Always Represent Valid States
- Types that represent both valid and invalid states are likely to lead to confusing and error-prone code.
- Prefer types that only represent valid states. Even if they are longer or harder to express, they will save you time and pain in the end.
1.5.2. Item 29: Be Liberal in What You Accept and Strict in What You Produce
type LngLat = { lng: number, lat: number } type LngLatLike = LngLat | { lon: number, lat: number } | [number, number] type Camera = { center: LngLat, zoom: number, bearing: number, pitch: number } type CameraOptions = Omit<Partial<Camera>, 'center'> & { center?: LngLatLike } type LngLatBounds = { northeast: LngLatLike, southwest: LngLatLike} | [LngLatLike, LngLatLike] | [number, number, number, number] declare function setCamera(camera: CameraOptions): void declare function viewportForBounds(bounds: LngLatBounds): Camera
- Input types tend to be broader than output types. Optional properties and union types are more common in parameter types than return types.
- To reuse types between parameters and return types, introduce a canonical form (for return types) and a looser form (for parameters).
1.5.3. Item 30: Don't Repeat Type Information in Documentation
- Consider including units in variable names if they aren't clear from the type (e.g., timeMS or temperatureC).
1.5.4. Item 31: Push Null Values to the Perimeter of Your Types
function extent(nums: number[]) { let result: [number, number] | null = null for (const num in nums) { if (!result) { result = [num, num] } else { result = [Math.min(num, result[0]), Math.max(num, result[1])] } } return result } const range = extent([0, 1, 2]) if (range) { const [min, max] = range //... } const [min, max] = range!
class UserPosts { user: UserInfo posts: Post[] constructor(user: UserInfo, posts: Post[]) { this.user = user this.posts = posts } static async init(userId: string): Promise<UserPosts> { const [user, posts] = await Promise.all([ fetchUser(userId), fetchPostsForUser(userId) ]) return new UserPosts(user, posts) } getUserName() { return this.user.name } }
- Avoid designs in which one value being
null
or notnull
is implicitly related to another value beingnull
or notnull
. - Push
null
values to the perimeter of your API by making larger objects eithernull
or fully non-null. This will make code clearer both for human readers and for the type checker. - Consider creating a fully non-null class and constructing it when all values are available.
- While
strictNullChecks
may flag many issues in your code, it's indispensable for surfacing the behavior of functions with respet to null values.
1.5.5. Item 32: Prefer Unions of Interfaces to Interfaces of Unions
- Interfaces with multiple properties that are union types are often a mistake because they obscure the relationships between these properties.
- Unions of interfaces are more precise and can be understood by TypeScript.
- Consider adding a "tag" to your structure to facilitate TypeScript's control flow analysis. Because they are so well supported, tagged unions are ubiquitous in TypeScript code.
1.5.6. Item 33: Prefer More Precise Alternatives to String Types
function pluck<T, K extends keyof T>(records: T[], key: K): T[K][] { return records.map(record => record[key]) }
- Avoid "stringly typed" code. Prefer more appropriate types where not every
string
is a possibility. - Prefer a union of string literal types to
string
if that more accurately describes tE domain of a variable. You'll get stricter type checking and improve the development experience. - Prefer
keyof T
tostring
for function parameters that are expected to be properties of an object.
1.5.7. Item 34: Prefer Incomplete Types to Inaccurate Types
- Avoid the uncanny valley of type safety: incorrect types are often worse than no types.
- If you cannot model a type accurately, do not model it inaccurately! Acknowledge the gaps using
any
orunknown
. - Pay attentation to error messages and autocomplete as you make typings increasingly precise. It's not just about correctness: developer experience matters, too.
1.5.8. Item 35: Generate Types from APIs and Specs, Not Data
- Consider generating types for API calls and data formats to get type safety all the way to the edge of your code.
- Prefer generating code from specs rather than data. Rare cases matter!
1.5.9. Item 36: Name Types Using the Language of Your Problem Domain
- Reuse names from the domain of your problem where possible to increase the readability and level of abstraction of your code.
- Avoid using different names for the same thing: make distinctions in names meaningful.
1.5.10. Item 37: Consider "Brands" for Nominal Typing
type AbsolutePath = string & { _brand: 'abs' } function isAbsolutePath(path: string): path is AbsolutePath { return path.startsWith('/') }
type SortedList<T> = T[] & { _brand: 'sorted' }
type Meters = number & { _brand: 'meters' } type Seconds = number & { _brand: 'seconds' } const meters = (m: number) => m as Meters const seconds = (s: number) => s as Seconds
- TypeScript uses structural ("duck") typing, which can sometimes lead to surprising results. If you need nominal typing, consider attaching "brands" to your values to distinguish them.
- In some cases you may be able to attach brands entirely in the type system, rather than at runtime. You can use this technique to model properties outside of TypeScript's type system.
1.6. Working with any
1.6.1. Item 38: Use the Narrowest Possible Scope for any Types
1.6.2. Item 39: Prefer More Precise Variants of any to Plain any
- When you use
any
, think about whether any JavaScript value is truly permissible. - Prefer more precise forms of
any
such asany[]
or{[id: string]: any}
if they more accurately model your data.
1.6.3. Item 40: Hide Unsafe Type Assertions in Well-Typed Functions
Sometimes unsafe type assertions are necessary or expedient. When you need to use one, hide it inside a function with a correct signature.
1.6.4. Item 41: Understand Evolving any
- While TypeScript types typically only refine, implicit
any
andany[]
types are allowed to envolve. You should be able to recognize and understand this construct where it occurs. - For better error checking, consider providing an explicit type annotation instead of using evolving
any
.
1.6.5. Item 42: Use unknown
Instead of any
for Values with an Unknown Type
The power and danger of any
come from two properties:
- Any type is assignable to the
any
type. - The
any
type is assignable to any other type.
The type checker is set-based, the use of any
effectively disables it.
unknown
:
- Any type is assignable to the
unknown
type. - The
unknown
type is only assignable tounknown
andany
.
never
:
- Nothing can be assigned to
never
. - The
never
type can be assigned to any other type.
1.6.6. Item 43: Prefer Type-Safe Approaches to Monkey Patching
export {} declare global { interface Document { monkey: string; } } document.monkey = 'Tamarin' interface MonkeyDocument extends Document { monkey: string } (document as MonkeyDocument).monkey = 'Tamarin'
interface FooWindow extends Window { foo: string } function isFooWindow(window: Window): window is FooWindow { return 'foo' in window } function foo(window: Window) { if (isFooWindow(window)) { window.foo } }
- Prefer structured code to storing data in globals or on the DOM.
- If you must store data on built-in types, use one of the type-safe approaches (augmentation or asserting a custom interface).
- Understand the scoping issues of augmentations.
1.6.7. Item 44: Track Your Type Coverage to Prevent Regressions in Type Safety
type-coverage --detail
- Even with
noImplicitAny
set,any
types acan make their way into your code either through explicitanys
or third-party type declarations. - Consider tracking how well-typed your program is. This will encourage you to revisit decisions about using
any
and increase type safety over time.
1.7. Types Declarations and @types
1.7.1. Item 45: Put TypeScript and @types in devDependencies
1.7.2. Item 46: Understand the Three Versions Involved in Type Declarations
- There are three versions involved in an
@types
dependency: the library version, the@types
version, and the TypeScript version. - If you update a library, make sure you update the corresponding
@types
.
1.7.3. Item 47: Export All Types That Appear in Public APIs
1.7.4. Item 48: Use TSDoc for API Comments
1.7.5. Item 49: Provide a Type for this
in Callbacks
function addKeyListener( el: HTMLElement, fn: (this: HTMLElement, e: KeyboardEvent) => void ) { el.addEventListener('keydown', e => { fn.call(el, e) }) } declare let el: HTMLElement addKeyListener(el, function(e) { this.innerHTML // "this" has type fo HTMLElement })
1.7.6. item 50: Prefer Conditional Types to Overloaded Declarations
function double<T extends number | string>(x: T): T function double(x: any) { return x + x } const a = double(1) // type of a is '1', not number const b = double('b') // type of b is 'b', not string
function double<T extends number | string>(x: T): T extends number ? number : string function double(x: any) { return x + x; } const a = double(1) // number const b = double('b') // string
Prefer conditional types to overloaded type declarations. By distributing over unions, conditional types allow your declarations to support union types without additional overloads.
1.7.7. Item 51: Mirror Types to Sever Dependencies
interface CsvBuffer { toString(encoding: string): string } // We just need Buffer.toString here. function parseCSV(contents: string | CsvBuffer): { [column: string]: string }[] { if (typeof contents === 'object') { // It's a Buffer return parseCSV(contents.toString('utf8')) } // ... return [] } // Buffer is Node.js's Buffer type parseCSV(new Buffer("column1,column2\nvalue1,value2", "utf-8"))
- Use structural typing to sever dependencies that are nonessential.
- Don't force JavaScript users to depend on
@types
. Don't force web developers to depend on NodeJS.
1.7.8. Item 52: Be Aware of the Pitfalls of Testing Types
function assertType<T>(value: T): T { return value } const a = 1 assertType<1>(a) // ok assertType<number>(a) // ok. a's type is 1, it's a subtype of number assertType<string>(a) // a's type is 1, it's not a subtype of string const double = (x: number) => x * 2 let p: Parameters<typeof double> = null! assertType<[number]>(p) // ok assertType<[number, number]>(p) // [number] is not assignable to [number, number] let r: ReturnType<typeof double> = null! assertType<number>(r) // ok declare function map<U, V>( array: U[], fn: (this: U[], item: U, index: number, array: U[]) => V ): V[] const beatles = ['John', 'Paul', 'George', 'Ringo'] assertType<number[]>(map( beatles, function(name, i, arr) { assertType<string>(name) // ok assertType<number>(i) // ok assertType<string[]>(arr) // ok assertType<string[]>(this) // ok return name.length } ))
- When testing types, be aware of the difference between equality and assignability, particularly for function types.
- For functions that use callbacks, test the inferred types of the callback parameters. Don't forget to test the type of
this
if it's part of your API. - Be aware of
any
in tests involving types. Consider using a tool likedtslint
for stricter, less error-prone checking.
1.8. Writing and Running Your Code
1.8.1. Item 53: Prefer ECMAScript Features to TypeScript Features
- Enums, parameter properties, triple-slash imports, and decorators are historical exceptions to this rule.
- In order to keep TypeScript's role in your codebase as clear as possible, I recommend avoiding these features.
1.8.2. Item 54: Know How to Iterate Over Objects
- Use
let k: keyof T
and a for-in loop to iterate objects when you know exactly what the keys will be. Be aware that any objects your function receives as parameters might have additional keys. - Use
Object.entries
to iterate over the keys and values of any object.
1.8.3. Item 55: Understand the DOM Hierarchy
- Know The Differences Between
Node
,Element
,HTMLElement
, AndEventTarget
, As Well As Those BetweenEvent
AndMouseEvent
. - Either Use A Specific Enough Type For DOM Elements And Events In Your Code Or Give TypeScript The Context To Infer It.
1.8.4. Item 56: Don't Rely On Private To Hide Information
- The
Private
Access Modifier Is Only Enforced Through The Type System. It Has No Effect At Runtime And Can Be Bypassed With An Assertion. Don't Assume It Will Keep Data Hidden. - For More Reliable Information Hiding, Use A Closure.
1.8.5. Item 57: Use Source Maps To Debug Typescript
1.9. Migrating to TypeScript
1.9.1. Item 58: Write Modern JavaScript
1.9.2. Item 59: Use @ts-check and JSDoc to Experiment with TypeScript
1.9.3. Item 60: Use allowJs to Mix TypeScript and JavaScript
1.9.4. Item 61: Convert Module by Module Up Your Dependency Graph
- Start migration by adding
@types
for third-party modules and external API calls. - Begin migrating your modules from the bottom of the dependency graph upwards. The first module will usually be some sort of utility code. Consider visualizing the dependency graph (madge tool) to help you track progress.
- Resist the urge to refactor your code as you uncover odd designs. Keep a list of ideas for future refactors, but stay focused on TypeScript conversion.
- Be aware of common errors that come up during conversion. Copy JSDoc annotations if necessary to avoid losing type safety as you convert.