← All lessons
TypeScript
All TypeScript snippets · 400
Every snippet has its own focus page with an explanation and the output it produces.
Basic Types · 24
- Declaring variables with primitive typesTypeScript primitives enforce the exact shape of data at compile time.
- Typing function parameters with primitivesUsing primitives in function signatures prevents passing the wrong data type into business logic.
- Assigning a string to a number variableTypeScript catches type mismatches during compilation before they cause runtime errors.
- Defining an array of numbersArray types ensure that every element within the collection shares the same type.
- Processing a list of user IDsThe generic Array<T> syntax is an alternative way to define arrays and is common in enterprise codebases.
- Pushing a wrong type to a typed arrayType safety extends to array mutations, preventing mixed types from slipping into collections.
- Declaring a fixed-length tupleTuples allow arrays with a fixed number of elements where each element has a specific, potentially different type.
- Using a tuple for an HTTP responseTuples are excellent for representing structured, predictable pairs of data like status codes and payloads.
- Tuples bypass length checks on pushTuple length constraints are not enforced by array methods like push, which can lead to unexpected runtime lengths.
- Defining a basic numeric enumEnums give friendly names to sets of numeric values, defaulting to zero-based indices.
- Using an enum for user rolesExplicitly assigning values to enums makes the compiled code clearer and prevents unintended shifts if items are added later.
- Numeric enum reverse mapping at runtimeNumeric enums generate a reverse mapping object at runtime, allowing value-to-key lookups which can be unexpected.
- Using 'any' to opt out of type checkingThe 'any' type disables type checking entirely, making it a dangerous escape hatch from TypeScript's safety.
- Typing JSON.parse output as unknownUsing 'unknown' instead of 'any' for untrusted data forces the consumer to narrow the type before using it.
- Attempting to use an 'unknown' type directlyUnlike 'any', 'unknown' requires type narrowing before you can call methods on it, preventing runtime crashes.
- Typing a function with no return value as voidThe 'void' type indicates that a function performs an action but does not return a value.
- Using void in a callback signatureCallbacks typed as void allow the caller to return anything without triggering a type error, ignoring the return value.
- Returning a value from a void callbackA void return type explicitly allows a function to return a value, which is a special exception designed for callback patterns.
- Assigning null and undefined to variablesIn TypeScript, null and undefined are distinct types that represent the intentional or unintentional absence of a value.
- Returning null when a user is not foundExplicitly returning null in a union type forces callers to handle the missing value case.
- Assigning null to a strict string variableWith strictNullChecks enabled, TypeScript prevents assigning null to types that do not explicitly include it.
- Using keyof on tuple typesApplying keyof to a tuple extracts the numeric indices as string literal types, allowing type-safe index access.
- Mapping over tuple indicesMapped types over tuples preserve their length and order, transforming the element types while keeping the tuple structure intact.
- Attempting string keyof on a tupleTuple indices are technically represented as string literals in TypeScript, but intersecting with 'string' can sometimes yield unexpected empty types depending on the context.
Variables and Declarations · 14
- Declaring mutable and immutable variables'let' allows reassignment while 'const' creates an immutable binding, enforcing safer variable usage.
- Defining API endpoints and retry countsConstants are used for configuration that never changes, while let tracks stateful mutable data.
- Mutating an object property declared with const'const' only prevents reassigning the variable binding, it does not freeze the object properties.
- Inferring a number type from assignmentTypeScript automatically deduces types from variable initialization, reducing boilerplate.
- Inferring object propertiesComplex object shapes are automatically inferred, making refactoring easier without explicit interfaces.
- Inferring a union type from conditional arraysIf an array contains mixed types on initialization, TypeScript infers a union array type, rejecting incompatible pushes.
- Asserting an any variable to a stringType assertions tell the compiler to treat a variable as a specific type, bypassing inference.
- Asserting DOM element typesAssertions are useful for untyped JSON payloads or DOM queries where you know the structure better than the compiler.
- Asserting to an incompatible type via unknownAssertions compile successfully but cause runtime errors if the actual type doesn't match the asserted type.
- Type widening of const variables to their literal typeConstants do not widen to their base type, whereas variables assigned from them do, causing unexpected type errors.
- let and const inside a blocklet and const are block-scoped, meaning they cannot be accessed outside of the curly braces where they were defined.
- Scope isolation in a loopUsing let in loops creates a new binding per iteration, preventing closure bugs and keeping variables scoped tightly.
- Accessing var outside of a blockUnlike let/const, var is function or globally scoped, leaking out of blocks like if-statements, which can cause bugs.
- Type assertion overriding the inferred typeType assertions force the compiler to accept a type, which hides compile errors but causes runtime crashes if incorrect.
Functions · 15
- Basic function with typed argumentsTyping both parameters and return values creates a strict contract for what the function accepts and produces.
- Calculating tax with typed numbersExplicit function types prevent math errors caused by string concatenation or undefined values.
- Returning a string from a number functionThe return type annotation is strictly enforced, preventing functions from leaking unexpected types.
- Using a default parameter valueDefault parameters allow functions to be called with fewer arguments by providing a fallback value.
- Sending an email with an optional subjectOptional parameters (denoted with ?) must come after required parameters and can be undefined.
- Placing a required parameter after an optional oneA required parameter cannot follow an optional one because it creates an ambiguous calling syntax.
- Summing values with a rest parameterRest parameters aggregate multiple arguments into a single typed array.
- Logging an event with variable tagsRest parameters allow for flexible APIs where an unknown number of arguments can be passed cleanly.
- Adding a parameter after a rest parameterA rest parameter must always be the last parameter in a function signature.
- Basic function overloads for different typesOverloads allow a single function to expose multiple signatures depending on input types.
- Padding a string or number differentlyOverloads provide accurate return types for different input parameters in utility functions.
- Calling an overload with an unsupported typeIf arguments do not match any provided overload signature, TypeScript throws a compile error.
- Typing 'this' in an object methodExplicitly typing 'this' ensures methods are only called in the correct context.
- Enforcing UI element contextTyping this prevents accidentally detaching methods from their host objects.
- Arrow function inheriting global 'this'Arrow functions capture 'this' from their surrounding scope, which is undefined in class/global strict mode.
Objects and Interfaces · 15
- Defining and using a simple interfaceInterfaces define the shape of an object, enforcing that all required properties are present.
- Typing a product objectInterfaces act as contracts across your codebase to ensure consistent data structures.
- Adding an excess property to an interfaceObject literals undergo excess property checking, failing if they contain properties not defined in the interface.
- Using optional and readonly modifiersOptional properties can be omitted, while readonly properties cannot be reassigned after creation.
- Defining an immutable user profileUsing readonly prevents accidental mutation of sensitive object properties during runtime.
- Attempting to reassign a readonly propertyTypeScript prevents reassignment of readonly properties at compile time, ensuring immutability.
- Passing an exact object literal to an interfaceFresh object literals are checked for exact structural matches against their target types.
- Failing to pass extra config to a functionExcess property checking catches typos and invalid properties when passing literals directly to functions.
- Bypassing excess property checks via variablesAssigning an object via a variable bypasses excess property checks as long as the required properties exist.
- Defining an object with dynamic string keysIndex signatures allow objects to have arbitrary keys as long as their values match the defined type.
- Creating a dynamic string-to-any cacheIndex signatures are ideal for hash maps or caches where keys are generated dynamically at runtime.
- Mixing incompatible explicit properties and index signaturesIf an interface has an index signature, all explicitly defined properties must match the index signature's type.
- Extending a base interfaceInterfaces can inherit properties from other interfaces, promoting code reuse and composition.
- Extending a BaseUser to create an AdminExtension is used to build specialized roles or variants from core domain models.
- Overriding an inherited property with an incompatible typeWhen extending, you cannot override a property with a type that is not assignable to the base property type.
Type Aliases vs Interfaces · 9
- Creating a simple type aliasType aliases create a new name for any type, including primitives, unions, and objects.
- Defining an object shape with a type aliasType aliases can define object shapes just like interfaces, offering a slightly different syntax.
- Assigning a boolean to a union type aliasType aliases strictly enforce their definitions, rejecting types not explicitly included in a union.
- Comparing type and interface syntaxBoth type aliases and interfaces can define object shapes, but they differ in extension capabilities.
- Using a type alias for a recursive JSON structureType aliases are better for complex recursive types or unions, which interfaces cannot directly express.
- Attempting to declare a duplicate type aliasUnlike interfaces, which merge on duplicate declarations, type aliases cannot be redeclared in the same scope.
- Defining a constructable type aliasType aliases can define constructor signatures using 'new (...)', allowing them to type class implementations dynamically.
- Dependency injection using constructor typesTyping constructors as variables allows passing classes around for dependency injection without relying on concrete implementations.
- Interfaces cannot define constructor signaturesWhile the syntax compiles, interfaces with 'new' signatures cannot be implemented by classes directly; you must use type aliases for class typing.
Union and Intersection Types · 9
- Defining a variable that can be string or numberUnion types allow a variable to hold values of multiple distinct types.
- Formatting a value that can be a string or numberUnion types are common in function parameters to accept flexible but restricted inputs.
- Calling a method on a union type without narrowingYou can only access properties that exist on all members of a union, requiring type narrowing for specific methods.
- Combining two object types with an intersectionIntersection types combine multiple types into one, requiring the value to satisfy all constituent types.
- Mixing mixins to create a LoggedUserIntersections are useful for composing capabilities or mixins into a single combined type.
- Creating an impossible type intersectionIntersecting conflicting property types results in the 'never' type, making the property unassignable without type assertions.
- Defining exact string literalsLiteral types restrict a variable to exact values, acting like a type-safe enum.
- Restricting function arguments to specific HTTP methodsLiteral unions are excellent for configuration values and finite sets of options.
- Assigning an invalid literal to a unionAny value outside the specified literal set will trigger a compile error.
Type Guards · 15
- Narrowing a union with typeofThe typeof operator narrows types within conditional blocks, granting access to type-specific methods.
- Processing dynamic input based on typeofType guards turn untyped or union inputs into safely typed operations at runtime.
- Using incorrect casing in typeof checksTypeScript's typeof checks are case-sensitive and must match the lowercase primitive names.
- Narrowing class instances with instanceofThe instanceof guard checks an object's prototype chain, narrowing it to a specific class.
- Handling specific error subclassesinstanceof is vital for distinguishing between custom error types or domain objects at runtime.
- Using instanceof on primitive stringsinstanceof only works on object types, not primitive literals, resulting in false at runtime.
- Narrowing object types with the in operatorThe in operator checks for the existence of a property, safely narrowing object union types.
- Distinguishing pet capabilities using inDuck-typing via the in operator is an effective way to differentiate objects based on unique methods.
- Checking for optional properties with inIf a property is optional, its presence via the in operator doesn't guarantee it isn't undefined.
- Creating a custom type predicate functionUser-defined type guards use 'x is T' syntax to allow custom logic to narrow types safely.
- Validating and narrowing a Fish objectCustom guards encapsulate complex runtime validation logic into reusable type-safe functions.
- A type guard that lies about the actual typeTypeScript trusts the type predicate blindly, leading to runtime crashes if the logic is incorrect.
- Narrowing a union based on a literal propertyDiscriminated unions use a shared literal property to easily and safely narrow complex object types.
- Handling network state with discriminated unionsState machines modeled as discriminated unions ensure all possible states are handled predictably.
- Failing to handle an unreachable case in a discriminated unionTypeScript uses exhaustiveness checking to flag code after all union cases have been handled.
Advanced Types · 9
- Defining a string or null unionNullable types explicitly mark where absence is a valid value, preventing null reference errors.
- Returning null from a failed database lookupExplicitly returning null communicates to the caller that a value might not exist.
- Accessing properties on a possibly null valueTypeScript prevents accessing properties on nullable types unless you explicitly check for null first.
- Safely accessing nested properties with ?.Optional chaining short-circuits to undefined if any part of the property chain is null or undefined.
- Safely accessing a nested user profileOptional chaining prevents runtime crashes when traversing deep object structures from APIs.
- Using optional chaining vs direct access on nullOptional chaining returns undefined safely, while direct access throws a runtime error if the object is null.
- Falling back to a default with ??The nullish coalescing operator provides a fallback only when the left side is null or undefined.
- Setting a default timeout configurationNullish coalescing is perfect for setting defaults for optional configuration properties.
- Confusing nullish coalescing with logical ORThe logical OR (||) overrides all falsy values like 0 or '', while nullish coalescing (??) only overrides null/undefined.
Classes · 27
- Defining a simple class with a methodClasses encapsulate data and behavior, providing a blueprint for creating objects.
- Creating an invoice entity classDomain entities use classes to bind relevant business logic directly to their data.
- Accessing a class property before initializationClass properties are undefined until assigned in the constructor, which can cause runtime errors if accessed prematurely.
- Using public, private, and protected modifiersAccess modifiers restrict visibility of class members, enforcing encapsulation and preventing external tampering.
- Protecting internal state of a bank accountPrivate modifiers prevent external code from directly modifying sensitive fields like a bank balance.
- Accessing a private member from outside the classTypeScript enforces private visibility at compile time, throwing an error if external code tries to access it.
- Shorthand property declaration in constructorParameter properties offer a concise way to declare and initialize class members directly in the constructor.
- Defining a User DTO with parameter propertiesParameter properties drastically reduce boilerplate when creating data transfer objects or models.
- Missing modifier prefix in parameter propertiesWithout an access modifier keyword in the constructor, the parameter is treated as a normal function argument, not a class property.
- Defining a readonly class propertyThe readonly modifier ensures a property can only be assigned during initialization or in the constructor.
- Setting a readonly ID in the constructorImmutable timestamps or IDs can be assigned once upon creation and never modified thereafter.
- Attempting to reassign a readonly propertyTypeScript prevents reassignment of readonly properties even from within the class methods.
- Creating an abstract class with an abstract methodAbstract classes cannot be instantiated directly and require subclasses to implement missing methods.
- Defining a base repository with abstract fetchAbstract classes define a template for subclasses, enforcing a contract while sharing concrete implementation.
- Instantiating an abstract class directlyTypeScript prevents creating instances of abstract classes because they are incomplete implementations.
- Using get and set to control accessGetters and setters intercept property access, allowing for validation or computed values.
- Validating an email address on setInterceptors allow you to apply validation rules whenever a property is updated.
- Stack overflow from recursive setterAssigning to the same property name inside a setter causes infinite recursion and a stack overflow at runtime.
- Accessing a static class propertyStatic members belong to the class itself rather than instances, accessed directly on the class name.
- Using a static factory methodStatic methods are often used for utility functions or factory methods that don't require instance state.
- Accessing a static member from an instanceStatic properties exist on the constructor function, not on the instance, leading to undefined when accessed via instance.
- Implementing an interfaceThe 'implements' keyword ensures a class adheres to an interface contract without inheriting implementation.
- Extending a base class while implementing an interfaceA class can inherit from one base class while implementing multiple interfaces to compose its shape.
- Extending an interface instead of implementing itInterfaces only describe shapes and contain no implementation, so they must be implemented, not extended, by classes.
- Initializing static fields in a static blockStatic initialization blocks run once when the class is initialized, allowing complex setup logic for static properties.
- Reading a file to initialize static configStatic blocks are excellent for computing static state that requires multi-line logic or try-catch handling at load time.
- Accessing instance properties in a static blockStatic blocks run in the context of the class constructor, not an instance, so 'this' refers to the class itself, not instance properties.
Generics · 18
- Writing a basic identity functionGenerics allow writing reusable functions where the input and output types are linked dynamically.
- Creating a generic API fetcherGenerics enable type-safe API calls by allowing the caller to specify the expected response shape.
- Assuming generic types have specific methodsGeneric types are treated as unknown shapes, preventing access to specific methods like toUpperCase without constraints.
- Defining a generic box interfaceInterfaces can accept type parameters, allowing flexible data structures like boxes, wrappers, or collections.
- Defining a generic pagination responseGeneric interfaces standardize wrapper structures, like pagination, across different domain models.
- Using an interface without providing type argumentsGeneric types require type arguments when instantiated, unlike generic functions which can infer them.
- Constraining a generic to have a length propertyConstraints (extends) restrict generics to types matching a specific shape, unlocking access to that shape's properties.
- Constraining a merge function to object typesConstraints ensure that functions expecting objects don't receive primitives, preventing runtime spread errors.
- Passing a primitive to a constrained object genericEven though primitives have methods, they are not considered objects structurally in this context, failing the constraint.
- Function with two independent type variablesFunctions can accept multiple type parameters to model relationships between different inputs and outputs.
- A generic event emitter mappingMultiple generics allow creating complex, type-safe mappings between event names and their callback payloads.
- Mismatching inferred type variablesWhen multiple parameters share the same type variable, TypeScript infers a union type or errors based on strictness.
- Providing a default type parameterDefault types allow generics to be used without explicit type arguments if a sensible default exists.
- Defaulting a generic response typeDefaulting to 'any' or 'unknown' preserves backwards compatibility when adding generics to existing interfaces.
- Default types not applying when explicit type is providedExplicitly providing a type argument overrides the default, breaking type inference from the arguments.
- Constraining a type by another type parameterUsing keyof with generics ensures that a function can only access keys that actually exist on the provided object.
- Type-safe property getter utilityThis pattern guarantees that invalid keys are caught at compile time, preventing undefined runtime lookups.
- Passing an invalid key to a keyof constrained functionThe constraint ensures the key exists, failing compilation if an arbitrary string is passed.
Utility Types · 24
- Making all properties optional with PartialPartial<T> constructs a type with all properties of T set to optional.
- Updating user profiles with patch payloadsPartial is ideal for PATCH request payloads where only a subset of fields are updated.
- Assuming Required unmakes optional properties during runtimeRequired<T> only enforces types at compile time; casting bypasses it, leading to runtime errors if data is missing.
- Making all properties readonlyReadonly<T> makes all properties of T immutable at the type level.
- Creating an immutable configuration objectApplying Readonly ensures configuration objects cannot be accidentally mutated during application runtime.
- Mutating a Readonly type via aliasingReadonly is a compile-time construct; if the underlying object is mutable, runtime aliasing bypasses the type checker.
- Picking specific properties from an interfacePick<T, K> creates a new type by selecting only the specified keys K from T.
- Creating a preview DTO using OmitOmit<T, K> removes sensitive or unnecessary keys, creating safe data transfer objects.
- Omitting a non-existent propertyUnlike Pick, Omit does not strictly check if the key exists on the original type, resulting in the same type.
- Creating a string-to-number dictionaryRecord<K, V> creates an object type where all keys are K and all values are V.
- Mapping status codes to messagesRecord is extremely useful for creating exhaustive lookup maps without defining a custom interface.
- Missing properties in an exhaustive RecordRecord enforces that all keys in the union must be present, preventing incomplete mappings.
- Excluding a type from a unionExclude<T, U> removes types from a union T that are assignable to U.
- Extracting only success response typesExtract<T, U> pulls matching shapes out of a discriminated union for specific handling.
- Extracting from incompatible unionsIf no members of the union match, Extract results in the 'never' type, making variables unassignable.
- Stripping null and undefined from a typeNonNullable<T> creates a new type by excluding null and undefined from T.
- Safely getting a value from a cacheNonNullable is useful for guaranteeing that downstream code won't crash on null or undefined values.
- Applying NonNullable to an already non-nullable typeApplying NonNullable to a clean type has no effect, returning the original type unchanged.
- Extracting the return type of a functionReturnType<T> extracts the type returned by a function, useful for wrapping or mapping functions.
- Extracting the parameters of a callbackParameters<T> extracts a tuple of parameter types, allowing dynamic argument handling utilities.
- Using ReturnType on an overloaded functionReturnType and Parameters only extract types from the last overload signature, ignoring earlier ones.
- Unwrapping a Promise typeAwaited<T> recursively unwraps Promises to get the type of the resolved value.
- Inferring the type of an async API callAwaited is commonly combined with ReturnType to get the resolved data type of async functions.
- Awaiting a non-Promise typeIf applied to a non-Promise type, Awaited simply returns the type itself without modification.
Mapped Types · 9
- Getting keys of an object as a unionThe keyof operator extracts the keys of an object type as a string or numeric literal union.
- Restricting function parameters to object keysUsing keyof guarantees that the key argument passed to the function actually exists on the object.
- Using keyof on primitive typesApplying keyof to a primitive returns the methods and properties available on that primitive's object wrapper.
- Creating a custom readonly mapped typeMapped types iterate over keys using 'in keyof' to transform properties dynamically.
- Creating a nullable version of an interfaceMapped types can wrap existing properties in unions to alter their allowed values.
- Mapped types altering value types incorrectlyMapped types override all properties unconditionally; to safely transform, you must preserve existing structures or use constraints.
- Removing readonly with a mapped typeModifiers like + or - can add or remove access modifiers like readonly in a mapped type.
- Making optional properties requiredThe minus modifier (-?) removes optionality, forcing all properties to be present.
- Failing to remove optionality at runtimeRemoving optionality via mapped types is a compile-time illusion; runtime undefined values still cause crashes if cast.
Conditional Types · 9
- Basic ternary type logicConditional types act like a ternary operator for types, returning one type or another based on a constraint.
- Inferring response type based on inputConditional types allow dynamic API shape definitions depending on the input parameter type.
- Distributive nature of conditional typesWhen fed a union, conditional types distribute over the union, resulting in an array for each type, not a single array of the union.
- Inferring the element type of an arrayThe infer keyword captures types inside a condition, like extracting the element type from an array.
- Extracting the resolved type of a PromiseInfer is heavily used in utility types to unwrap containers like Promises or Arrays safely.
- Inferring from a non-matching typeIf the condition doesn't match (e.g. string isn't a Promise), the infer branch isn't taken, yielding never or the false branch.
- Filtering types out of a unionDistribution allows conditional types to filter unions by returning never for non-matching members.
- Removing null from a type using distributionDistributive conditionals evaluate each member of a union independently, allowing precise type stripping.
- Preventing distribution with tuple wrappingWrapping types in tuples prevents distribution, resulting in a single array of the union type rather than a union of arrays.
Type Compatibility · 12
- Assigning an object with extra properties to a typeTypeScript uses structural typing; an object is compatible if it has all required properties, regardless of excess ones.
- Structural compatibility between two different classesClasses are structurally compared; B is assignable to A if B has all properties A requires.
- Missing required properties in structural typingDespite structural flexibility, target types must have all required properties present in the source object.
- Assigning a function with fewer parametersFunctions with fewer parameters are assignable to types expecting more parameters, safely ignoring extras.
- Using a single argument callback in a mapArray.prototype.map expects a 3-argument callback, but passing a 1-argument function is safe and common practice.
- Assigning a function with more parametersFunctions requiring more parameters cannot be assigned to types expecting fewer, as the extra parameter would be undefined at runtime.
- Contravariance of function parametersUnder strictFunctionTypes, a function expecting a subtype cannot be assigned where a base type function is expected.
- Event handler compatibilityA general handler (Event) is safely assignable to a specific handler (ClickEvent) because it expects less specific data.
- Method bivariance vs strict function typesClass methods are checked bivariantly for practicality, meaning strict contravariance does not apply to them, unlike function properties.
- Fresh object literal excess property checkFresh object literals are strictly checked for excess properties when directly assigned or passed to functions.
- Bypassing fresh object checks via variablesOnce an object literal is assigned to a variable, it loses its 'freshness', and excess properties are ignored in subsequent assignments.
- Excess property check triggered on nested fresh objectsExcess property checks apply recursively to nested fresh object literals during assignment.
Modern TypeScript · 3
- Using satisfies to enforce type conformityThe satisfies operator ensures an expression matches a type without changing the inferred type of the expression.
- Type-safe theme configuration with satisfiesSatisfies guarantees the object matches the Record type while preserving the literal types of specific keys.
- Losing literal types by annotating instead of satisfiesDirect annotation widens the inferred type of 'a' to the union, whereas satisfies would preserve it as the literal 1.
Type Manipulation · 21
- Creating a type from string literalsTemplate literal types allow constructing new string literal types by combining existing unions via syntax similar to string interpolation.
- Generating event listener names from propertiesTemplate literals dynamically generate precise string types, ensuring event names strictly match the properties of an object.
- Assigning a raw string to a template literal typeTemplate literal types enforce exact structural patterns, rejecting strings that don't match the specified literal and numeric placeholders.
- Uppercasing a string literal typeTypeScript provides built-in utility types like Uppercase to transform string literal types at compile time.
- Creating a constant case from camelCaseCombining string manipulation utilities allows transformation of naming conventions strictly at the type level.
- String manipulation utils only work on literal typesWhen applied to the generic 'string' type rather than a specific literal, utilities like Uppercase have no effect.
- Remapping keys in a mapped typeKey remapping via the 'as' clause allows transforming the names of properties while iterating over them in mapped types.
- Creating a setter interface from a classKey remapping is essential for generating boilerplate APIs, like setters or getters, directly from existing data models.
- Remapping keys to incompatible typesThe 'as' clause must evaluate to a string, number, or symbol; remapping to arbitrary types causes compiler errors.
- Inferring parts of a string literalThe infer keyword can be used inside template literal types to extract and capture portions of a string as a new type.
- Parsing a route path into parametersTemplate literal inference enables strongly-typed routing systems where path parameters are extracted automatically.
- Failing to infer from a non-matching templateIf the target string does not match the template pattern, the conditional type resolves to the false branch.
- Filtering out keys using 'as never'By remapping keys to 'never' in a mapped type, you can dynamically remove specific properties from an object type.
- Creating a public view by stripping private keysPattern matching in key remapping allows removing entire groups of keys, like internal properties prefixed with an underscore.
- Failing to remove keys due to incorrect conditional logicIf the conditional logic doesn't evaluate to 'never' for the targeted key, the key remains in the resulting type.
- Constraining an inferred typeThe 'infer extends' syntax constrains the inferred type, ensuring it matches a specific shape before assigning it.
- Extracting a specific event typeConstraining inferred types is useful when extracting sub-types from complex structures that must adhere to a specific literal union.
- infer extends failing the constraintIf the inferred type does not satisfy the constraint, the conditional type resolves to the false branch (never).
- Getting the constructor type of a classThe typeof operator on a class name extracts the type of the class constructor itself, rather than an instance type.
- Passing class constructors to functionsUsing typeof allows functions to accept classes (constructors) instead of instances, useful for factory or dependency injection patterns.
- Assigning an instance to a typeof class variableBecause typeof User represents the constructor, an instance cannot be assigned to it; instances are typed by the class name directly.
Modules & Namespaces · 15
- Exporting and importing a typed variableTypeScript fully supports ES module syntax, allowing types and implementations to be exported and imported across files.
- Exporting an interface and implementationSeparating interfaces from implementations in modules promotes cleaner architecture and better testability.
- Importing a type without 'type' modifier in isolatedModulesUnder isolatedModules, re-exporting a type without the 'type' modifier can cause runtime errors because transpilers can't distinguish types from values.
- Defining a default exportDefault exports allow a module to export a single primary feature, imported with any name without curly braces.
- Default exporting a class instanceDefault exports are often used for singleton instances or main application configurations.
- Mixing default and named imports incorrectlyWhile 'default as' works, standard practice is importing the default directly before the named imports block to avoid syntax errors.
- Defining a basic namespaceNamespaces group related code under a single global object, an older pattern predating ES modules.
- Using namespaces for internal utility groupingNamespaces are still useful in single-file applications or bundling internal utilities without module overhead.
- Forgetting to export a namespace memberWithout the 'export' keyword inside a namespace, members are private to the namespace and inaccessible externally.
- Augmenting an existing module interfaceModule augmentation allows extending types defined in other modules without modifying the original source code.
- Adding a custom method to Express RequestAugmenting third-party library types is a common pattern for adding middleware-injected properties in web frameworks.
- Augmenting a module that doesn't existTypeScript will silently create an empty module if the path doesn't resolve, leading to runtime import failures if not careful.
- Declaring an ambient module for untyped JSAmbient module declarations provide types for JavaScript libraries that lack their own type definitions.
- Creating a wildcard module declarationWildcard ambient modules are useful for typing Webpack or Vite loaders that import non-JS assets as strings.
- Importing from an ambient module with missing exportsEven with ambient declarations, TypeScript strictly enforces that imports must match the declared exports of the module.
Decorators · 12
- Applying a basic class decoratorClass decorators are functions applied via the @ syntax that can modify or replace the class constructor.
- Injecting metadata into a classDecorator factories accept arguments to configure behavior, often used to attach routing or ORM metadata to classes.
- Decorator returning an invalid typeA class decorator must return either void or a constructor function; returning a primitive causes a runtime TypeError.
- Logging method calls with a decoratorMethod decorators intercept method execution by modifying the PropertyDescriptor, enabling cross-cutting concerns like logging.
- Depracating a method safelyDecorators provide an elegant way to intercept calls and inject warnings or telemetry without modifying core logic.
- Losing 'this' context in method decoratorsReplacing a method with an arrow function detaches it from the instance's 'this' context, leading to undefined errors.
- Basic property decoratorProperty decorators receive the target and key but cannot easily modify the value, often used for metadata registration.
- Registering validation rules via property decoratorsProperty decorators are heavily used in validation libraries to attach rules that are checked later by a validator.
- Property decorators cannot intercept initializationUnlike getters/setters, property decorators do not intercept assignments; they only run once during class definition.
- Marking a parameter with a decoratorParameter decorators execute when the method is defined, providing the parameter index for metadata injection.
- Extracting route parameters via parameter decoratorsFrameworks like NestJS use parameter decorators to identify which arguments should be parsed from the HTTP request.
- Accessing parameter values inside a parameter decoratorParameter decorators only have access to the positional index, not the runtime value of the argument passed.
Async & Promises · 12
- Defining a function returning a typed PromiseExplicitly typing Promise<T> ensures that the consumer knows exactly what data type the asynchronous operation resolves with.
- Fetching user data with typed async/awaitCombining async/await with typed Promises provides end-to-end type safety for asynchronous data flows.
- Forgetting to await a PromiseFailing to await an async function assigns the Promise object itself, causing a type mismatch between Promise<number> and number.
- Awaited type behavior in async functionsAsync functions automatically wrap their return type in a Promise, but Awaited can unwrap it back to the inner type.
- Returning a Promise of a PromiseTypeScript automatically flattens nested Promises, so awaiting an async function returning a Promise resolves to the inner value.
- Rejecting a Promise with an untyped errorIn TypeScript, caught errors are typed as 'any' or 'unknown' by default, requiring type narrowing before accessing properties.
- Typing Promise.all with mixed typesPromise.all preserves the order and types of the input promises in a strongly typed tuple array.
- Fetching parallel resources with Promise.allUsing typed async functions with Promise.all allows destructuring the results with full type inference across parallel tasks.
- Promise.race returning a union of typesUnlike Promise.all, Promise.race resolves with the value of the first settled promise, so the type is a union of all inputs.
- Try/catch block in async functionTry/catch works seamlessly with async/await, allowing linear error handling for asynchronous operations.
- Wrapping API calls in a Result type patternReturning a discriminated union from async functions eliminates the need for try/catch blocks in the consumer code.
- Unhandled promise rejection crashing the appIf an awaited promise throws and isn't caught, it becomes an unhandled rejection, which crashes Node.js applications.
tsconfig & Compiler · 12
- Enabling strict mode in tsconfigStrict mode enables a set of type-checking options that force developers to write safer, more predictable code.
- Strict null checks preventing undefined accessUnder strict mode, array methods like find return T | undefined, forcing the developer to check for undefined before use.
- Implicit any in function parameters under strictStrict mode disables implicit any, requiring explicit type annotations for all function parameters.
- Preventing null assignment to stringsstrictNullChecks separates null and undefined from other types, preventing a large class of runtime null reference errors.
- Checking for null before accessing propertiesWith strictNullChecks, you must explicitly check that optional or nullable variables are defined before accessing their properties.
- Array index access returns T instead of T | undefinedBy default, TypeScript assumes array access returns T even for out-of-bounds indices; noUncheckedIndexedAccess fixes this.
- Catching implicit any in variablesnoImplicitAny forces explicit typing for parameters, preventing unsafe 'any' types from silently spreading in the codebase.
- Typing JSON.parse to avoid implicit anyWhile JSON.parse returns any, passing it around without typing triggers noImplicitAny errors when consumed in strict contexts.
- Implicit any in catch clausesWithout useUnknownInCatchVariables, caught errors are typed as any, bypassing noImplicitAny and hiding potential runtime issues.
- Re-exporting types safely under isolatedModulesisolatedModules enforces that each file can be transpiled independently, requiring explicit 'type' markers for type-only exports.
- Const enums issue with isolatedModulesConst enums are inlined by the compiler, which breaks single-file transpilation; isolatedModules flags them as errors.
- Mixing type and value imports under isolatedModulesIf 'User' is a type and 'createUser' is a value, some transpilers drop the type, but mixing them requires explicit handling.
Advanced Generics · 12
- Array covariance in TypeScriptTypeScript arrays are covariant, meaning an array of subtypes can be assigned to an array of base types.
- Function parameter contravarianceA function accepting a base type (Animal) can be assigned to a function expecting a subtype (Dog), known as contravariance.
- Unsafe array covariance allowing runtime errorsBecause arrays are covariant, pushing a base type into an array of subtypes compiles, but causes a runtime error when subtype methods are called.
- Defining a linked list node typeRecursive types reference themselves within their own definition, essential for tree or list data structures.
- Typing a deeply nested JSON structureRecursive unions allow accurate typing of complex dynamic data like arbitrary JSON objects.
- Infinite recursion in conditional typesRecursive type aliases that don't debounce or hit a base case can cause the compiler to crash or hang.
- Inferring only the first type argumentWhen multiple arguments share a generic but are passed different types, TypeScript infers a union or the base type.
- Failing to infer from nested object propertiesTypeScript is usually good at inferring from nested structures, but deep or complex wrappers can require explicit annotation.
- Inferring union types instead of overloads in genericsGenerics don't create overloads for mixed types; they infer a union, meaning the return type is the union, not the specific input type.
- Attempting to use class generics in static methodsStatic members cannot reference the class's generic type parameter because generics are tied to instances, not the class constructor.
- Static factory with its own generic parameterTo use generics in static factories, define a new generic parameter (e.g., U) specific to that static method.
- Accessing static members via a generic instanceYou cannot access static members via an instance type parameter (c.count); you must use the class constructor itself.
Symbols & Iterators · 12
- Declaring a unique symbolSymbols create completely unique property keys that won't collide with any string-based properties.
- Using Symbol.iterator to make an object iterableImplementing Symbol.iterator allows custom objects to work with for..of loops and the spread operator.
- Accessing symbol properties with string keysSymbols are completely invisible to string property access and JSON.stringify, providing true privacy.
- Implementing a basic iterator interfaceAn iterator is an object with a next() method returning {value, done}, conforming to the IterableIterator interface.
- Custom collection iterator with done stateProperly implementing the done state ensures loops and spread operators terminate correctly without infinite loops.
- Forgetting to return done: trueIf an iterator never returns done: true, for..of loops will run infinitely until the call stack overflows.
- Creating a basic generator functionGenerators provide a simpler syntax for creating custom iterators using the function* and yield keywords.
- Generating an infinite sequence lazilyGenerators pause execution at yield, allowing infinite sequences to be consumed lazily without blocking or memory overflow.
- Typing the return value of a generatorThe Generator type takes (TYield, TReturn, TNext); the final next() returns an object with the TReturn value.
- Using Symbol.for for global symbolsSymbol.for accesses a global symbol registry, returning the exact same symbol for the same key across different scopes.
- Cross-module singleton via global symbolGlobal symbols allow different modules or scripts to share hidden properties on objects without string key collisions.
- Comparing local Symbol() with Symbol.for()Local Symbol() calls always create unique symbols, even if they share the same description; they will never equal global Symbol.for() instances.
Error Handling · 9
- Extending the base Error classCustom error classes allow throwing specific error types that can be uniquely identified in catch blocks.
- Adding contextual data to custom errorsAttaching extra metadata to custom errors provides valuable context for logging and debugging without parsing message strings.
- Prototype chain breaking in transpiled ES5 errorsWhen targeting ES5, extending built-in classes like Error can break instanceof unless you manually set the prototype.
- Catch clause typed as unknownWith useUnknownInCatchVariables, caught errors are unknown, forcing developers to narrow the type before interacting with it.
- Using instanceof to narrow caught errorsinstanceof is the safest way to narrow an unknown caught error down to a standard Error object with a message property.
- Accessing message property on unknown errorBecause the error is unknown, TypeScript prevents accessing properties like .message without explicit type narrowing.
- Throwing a string literalWhile JavaScript allows throwing any value, throwing strings is considered bad practice as it lacks stack traces.
- Throwing a custom error instanceThrowing instances of Error or its subclasses provides stack traces and structured data for error handling logic.
- TypeScript does not enforce thrown typesTypeScript's return type annotations do not include thrown errors, and a throw statement satisfies any return type.
Contextual Typing · 4
- Contextual typing of arrow functionsTypeScript infers the parameter types of arrow functions based on the type of the variable they are assigned to.
- Contextual typing in array map callbacksArray methods contextually type their callbacks, allowing the parameter 'x' to be inferred as a number without explicit annotation.
- Contextual typing failure with union typesWhen the contextual type is a union of function types, TypeScript cannot contextually type the parameter, requiring explicit annotation.
- Contextual typing of object methodsObject methods are contextually typed by their interface definition, allowing the parameter 'n' to be inferred as string.
Inference Deep Dive · 6
- Preventing literal widening with as constThe 'as const' assertion tells TypeScript to infer the most specific literal type possible, preventing widening to the base type.
- Creating an immutable configuration objectApplying 'as const' to an object makes all properties readonly and infers literal types, freezing the configuration shape.
- Attempting to mutate an as const objectArrays declared with 'as const' become readonly tuples, preventing mutation methods like push at compile time.
- Using const modifier on a generic type parameterThe 'const' modifier on a generic parameter infers literal types without requiring the caller to use 'as const'.
- Preserving literal unions in a generic functionUsing 'const' ensures that the literal type 'success' is preserved through the generic, rather than widening to 'string'.
- Const type parameters forcing immutabilityWhen 'const' is used, the inferred type is a readonly tuple, causing errors if the function attempts to mutate the argument.
Advanced Tuples · 6
- Concatenating two tuples genericallyVariadic tuple types allow spreading generic type parameters into other tuples, preserving length and order dynamically.
- Prepending an element to a generic tupleSpreading a generic tuple U after a fixed type T creates a new tuple with T at the front and U's elements following.
- Unbounded tuple spreadingWhile allowed, placing a rest element before a final fixed element can confuse inference and should be done carefully.
- Defining a tuple with named labelsLabeled tuple elements provide documentation and autocomplete for indices without changing the underlying array structure.
- Using labeled tuples for parameter destructuringLabeled tuples can define complex function signatures cleanly, making destructured arguments self-documenting.
- Labels do not create object propertiesTuple labels are strictly for editor tooling and documentation; the runtime value remains an array with numeric indices.
Strict Compiler Flags · 9
- Failing to initialize a class propertyUnder strict mode, class properties must be initialized in the constructor or have a default value.
- Initializing properties via the constructorAssigning a property inside the constructor satisfies the strict initialization check.
- Using the definite assignment assertion operatorThe '!' operator overrides the strict check, promising TypeScript that the property will be initialized externally, bypassing safety.
- Type-safe usage of callStrict bind call apply ensures that arguments passed to .call, .bind, or .apply match the original function signature.
- Binding a method with correct context and argumentsThe .bind method contextually types its arguments based on the function being bound, preventing mismatched parameters.
- Passing wrong types to call under strict modeWithout the flag, .call accepts any arguments; with it, TypeScript catches the string-to-number mismatch.
- Declaring an unused variablenoUnusedLocals throws an error if a variable is declared but never read, keeping the codebase clean.
- Ignoring unused parameters with underscorePrefixing a parameter with an underscore tells TypeScript that it is intentionally unused, bypassing the check.
- Unused imports triggering errorsnoUnusedLocals also applies to imports; if a type or value is imported but never used, the compiler will flag it.
Control Flow & Narrowing · 9
- Using never to ensure exhaustive switchesAssigning the switch value to 'never' in the default case forces a compile error if a new union member is added but not handled.
- AssertNever pattern for state machinesThe assertNever utility provides a runtime fallback while guaranteeing compile-time exhaustiveness for discriminated unions.
- Failing exhaustive check after adding a union memberIf 'triangle' is added to the union but not handled in the switch, 's' is no longer of type 'never' in the default case, causing a compile error.
- Narrowing via switch(true)switch(true) allows narrowing complex conditions by matching the truthiness of expressions, useful for type guards.
- Categorizing values with switch(true)This pattern is excellent for replacing long if-else chains with a cleaner, more declarative switch structure.
- Loss of narrowing in non-strict equalityTypeScript sometimes fails to narrow types correctly when using loose equality (==) inside switch(true) conditions.
- Storing a type guard in a variableTypeScript tracks aliased boolean conditions, allowing type narrowing even when the type guard is extracted to a variable.
- Aliasing discriminant checksStoring discriminant checks in variables preserves narrowing, making complex conditional logic much easier to read and write safely.
- Breaking alias narrowing via reassignmentIf an aliased condition is reassigned later in the function, TypeScript discards the narrowing relationship to ensure safety.
Advanced Classes & OOP · 9
- Basic mixin application via intersectionMixins allow composing classes from reusable components by extending a base class within a function, merging their features.
- Adding a log method via mixinMixins are a powerful pattern for adding cross-cutting concerns, like logging or timestamping, to multiple classes without inheritance bloat.
- Mixins losing type information of base classIf generic constraints are too loose, the mixin might not preserve the base class methods; proper constraints are required for full typing.
- Typing a function that accepts an abstract classTypeScript allows typing abstract constructors using 'abstract new', enabling factories that accept abstract classes but return instances.
- Mixin factory accepting abstract base classesUsing abstract constructor signatures in mixins allows extending unimplemented base classes while adding concrete properties.
- Directly instantiating an abstract constructor typeEven if a function accepts an abstract constructor, TypeScript prevents direct instantiation inside the function without a concrete subclass.
- Returning the current class instance typeThe 'this' type allows methods to return the instance of the exact class they are called on, enabling fluent interfaces and chaining.
- Builder pattern using polymorphic thisUsing 'this' in inherited methods ensures that chaining returns the subclass instance, preserving access to subclass methods.
- Polymorphic this breaking type inference when extractedWhen extracting methods that use 'this' types, the contextual 'this' is lost, making it difficult to type the standalone function.
Modern ES Features · 6
- Resource management with usingThe 'using' keyword (ES2024) automatically calls Symbol.dispose when the block scope ends, ensuring cleanup.
- Managing a database connection via usingUsing declarations guarantee that resources like files or database connections are closed even if exceptions are thrown.
- Forgetting to implement Symbol.disposeObjects passed to 'using' must implement either Symbol.dispose or Symbol.asyncDispose, or TypeScript will throw a compile error.
- Defining an auto-accessor fieldThe 'accessor' keyword (TC39 Stage 3) automatically wraps the property in a getter and setter, storing the value in a private field.
- Intercepting assignments with accessorAuto-accessors provide a clean syntax for encapsulating state while allowing future interception via decorators without changing the public API.
- Auto-accessor privacy conflictsThe 'accessor' keyword generates its own private backing field; mixing it with manual private fields can cause naming conflicts or shadowing.
Advanced Modules · 12
- Typing a dynamic importDynamic imports return a Promise typed with the module's exports, enabling code splitting and lazy loading.
- Conditionally loading an analytics moduleDynamic imports allow conditionally loading heavy modules only when needed, reducing initial bundle size.
- Accessing non-existent exports from a dynamic importTypeScript resolves the module's type definition during compilation, throwing an error if the dynamically imported export doesn't exist.
- Using import type for interfaces'import type' explicitly imports only type information, which is completely erased during compilation to JavaScript.
- Mixing type and value imports inlineInline 'type' modifiers allow importing values and types from the same module while ensuring types are stripped by the transpiler.
- Attempting to use a type-only import as a valueBecause 'import type' is erased at runtime, using it as a value (like instantiating a class) causes a runtime ReferenceError or compile error.
- Default importing a CommonJS moduleWith esModuleInterop enabled, TypeScript allows default importing CommonJS modules, treating module.exports as the default export.
- Using require in TypeScriptWhile not idiomatic, you can use CommonJS 'require' by declaring it, though it bypasses TypeScript's module type checking.
- Namespace import failing without esModuleInteropWithout esModuleInterop, namespace imports of CommonJS modules cannot be called directly; you must use the .default property.
- Configuring path mapping in tsconfigPath mapping allows defining aliases for directories, avoiding deep relative imports like '../../../'.
- Importing services using an aliasAliases make imports cleaner and easier to refactor when moving files around in large projects.
- Paths not resolving at runtime without bundlerTypeScript paths are purely a compile-time concept; at runtime in Node.js, the alias '@/' will fail unless a bundler or ts-node registration resolves it.
Async Iteration · 6
- Implementing an async iteratorImplementing Symbol.asyncIterator allows an object to be consumed by for-await-of loops, yielding values asynchronously.
- Consuming an async stream with for awaitThe for-await-of loop pauses execution until the next promise from the async iterator resolves, ideal for streaming data.
- Attempting to use for-of on an async iteratorStandard for-of loops cannot handle async iterators; you must use for-await-of to consume them correctly.
- Typing an async generator functionAsyncGenerator types take TYield, TReturn, and TNext, defining the shape of yielded promises and the final return value.
- Polling an API asynchronouslyAsync generators are perfect for creating infinite polling loops that can be paused and canceled by stopping iteration.
- Passing wrong types to async generator next()The third type parameter of AsyncGenerator dictates what type can be passed back into next(); passing 42 to a string TNext causes an error.
Type System Limits · 9
- Contrasting any, unknown, and never'any' opts out of checking, 'unknown' is a safe top type requiring narrowing, and 'never' is the bottom type that cannot hold a value.
- Using never to filter union typesBecause 'never' is absorbed by unions, returning it in conditional types effectively removes unwanted members from the union.
- Assigning values to a never variableNo value can be assigned to 'never' because it represents an impossible state or function that never returns.
- Deeply nested type instantiationTypeScript has a recursion limit to prevent infinite loops in the compiler, usually around 50-100 levels deep depending on the operation.
- Exceeding tuple recursion limitsBuilding large tuples via recursion is a common pattern for integer math in types, but hits instantiation depth limits quickly.
- Infinite conditional type distributionConditional types that continuously wrap themselves without a base case cause the compiler to give up and throw an instantiation error.
- Defining a circular type referenceTypeScript allows types to reference themselves directly, which is essential for linked lists and tree structures.
- JSON schema circular referencesRecursive type aliases and interfaces handle nested schemas flawlessly, as long as the compiler can lazily evaluate them.
- Immediate circular self-reference causing errorAn immediate circular alias without any structure causes a compiler error because the type cannot be resolved.
Global Scope · 3
- Augmenting the global scopeThe 'declare global' syntax inside a module file allows adding variables or types to the global namespace.
- Adding a custom Error constructor globallyAugmenting global interfaces like Window is common in browser environments to inject typed global variables from libraries.
- Using declare global outside a moduleGlobal augmentation only works inside ES modules; in a script file, TypeScript throws an error because there are no top-level imports/exports.
Namespaces & Merging · 3
- Adding static properties to a function via namespaceDeclaring a namespace with the same name as a function merges them, allowing static properties to be attached to the function object.
- Creating a factory function with typed configThis pattern is heavily used in older JavaScript libraries (like jQuery or Express) to attach configuration or utilities directly to the main callable function.
- Namespace merging failing on arrow functionsFunction-namespace merging only works with standard function declarations, not const arrow functions, due to how hoisting and merging occur.