Welcome back, intrepid TypeScript explorer! You’ve come a long way, mastering types, interfaces, generics, and even some advanced patterns. That’s fantastic! But here’s a little secret: even the most seasoned developers stumble from time to time. TypeScript is a powerful tool, but like any powerful tool, it has nuances that can lead to common pitfalls if we’re not careful.

In this chapter, we’re going to shine a light on some of the most frequent mistakes developers make when working with TypeScript. More importantly, we’ll equip you with the knowledge and strategies to recognize these pitfalls, understand why they’re problematic, and apply robust solutions to avoid them. Our goal isn’t just to fix errors, but to foster a deeper understanding that prevents them from happening in the first place, making your code more reliable and easier to maintain.

To get the most out of this chapter, you should be comfortable with basic to intermediate TypeScript concepts, including defining types and interfaces, working with functions, understanding basic generics, and navigating your tsconfig.json. Don’t worry if something feels a little fuzzy; we’ll review concepts as needed. Let’s dive in and learn how to write truly resilient TypeScript!


Core Concepts: Recognizing and Resolving Common Traps

TypeScript’s primary mission is to catch errors before your code ever runs. However, sometimes we inadvertently give TypeScript ways to “look the other way,” or we make assumptions that lead to problems. Let’s explore some of the biggest culprits.

Pitfall 1: Over-Reliance on any (The “Escape Hatch” that Hides Problems)

Imagine you’re building a super-secure, high-tech vault. TypeScript is like the intricate lock mechanism, ensuring only authorized items (correct types) go in or out. The any type, however, is like a secret override button that opens the vault for anything without checking.

What is any? The any type is TypeScript’s most flexible type. When a variable or expression is typed as any, TypeScript essentially says, “Okay, I give up. You can do anything with this value, and I won’t perform any type checking.” This means you can assign any value to it, call any method on it, or access any property, and TypeScript will happily compile your code without complaint.

Why is it a Pitfall? While any seems convenient, it completely defeats the purpose of using TypeScript. It turns TypeScript code back into plain JavaScript, losing all the benefits of type safety. This means potential runtime errors that TypeScript should have caught are now free to sneak into your production environment. It’s like having a safety net, but choosing to jump without it.

Consider this example:

function processData(data: any) {
  // TypeScript won't complain here, even if `data` doesn't have a `name` property
  console.log(data.name.toUpperCase());
}

processData({ age: 30 }); // This will crash at runtime! data.name is undefined.

If data were explicitly typed, TypeScript would immediately flag an error. With any, it sails through compilation, only to fail spectacularly when the program runs.

The Solution: Embrace unknown and Type Narrowing Instead of any, the modern and much safer alternative is unknown. unknown is like any in that it can hold any value, but it’s much stricter. If a variable is unknown, you cannot perform any operations on it (like accessing properties or calling methods) until you’ve narrowed its type.

Think of unknown as a sealed box. You know there’s something inside, but you can’t use it until you’ve opened the box and verified what it is.

Let’s refactor the previous example using unknown:

function processStrictData(data: unknown) {
  // Error: Object is of type 'unknown'.
  // console.log(data.name.toUpperCase()); // TypeScript immediately complains!

  // We must first narrow the type
  if (typeof data === 'object' && data !== null && 'name' in data && typeof (data as { name: unknown }).name === 'string') {
    // Now TypeScript knows `data` has a `name` property which is a string
    console.log((data as { name: string }).name.toUpperCase());
  } else {
    console.warn("Invalid data format for processing.");
  }
}

processStrictData({ name: "Alice", age: 30 }); // Works fine
processStrictData({ age: 30 });             // Now safely caught by our runtime check
processStrictData("hello");                 // Also safely caught

Notice how unknown forces us to perform runtime checks (type guards) to ensure the data has the expected structure before we can safely interact with it. This is TypeScript working with you, not against you, to prevent bugs.

Pitfall 2: Incorrect Type Assertions (as Type) (Trusting Too Much)

Type assertions are another “override” mechanism, but a more specific one. When you use value as Type, you’re telling TypeScript, “Hey, I know this value might not look like Type right now, but trust me, it is Type.”

What is a Type Assertion? It’s a way to explicitly tell the TypeScript compiler about the type of a variable when TypeScript’s inference isn’t enough, or when you have more specific knowledge about the type than the compiler does.

const myCanvas = document.getElementById('myCanvas') as HTMLCanvasElement;
// Here, we're asserting that 'myCanvas' is definitely an HTMLCanvasElement
// because getElementById returns a generic HTMLElement or null.

Why is it a Pitfall? The danger comes when your assertion is wrong. If document.getElementById('myCanvas') returns null or an HTMLDivElement, and you assert it as HTMLCanvasElement, TypeScript will compile without error. However, at runtime, trying to access myCanvas.getContext() will lead to a runtime error (e.g., “Cannot read properties of null” or “getContext is not a function”).

You’re essentially telling TypeScript to stop checking for that specific type. If you’re wrong, TypeScript can’t help you.

The Solution: Use Type Guards and Narrowing Just like with unknown, type guards are your best friends here. Instead of blindly asserting, verify the type at runtime.

const myElement = document.getElementById('myCanvas');

// Check if the element exists AND if it's an instance of HTMLCanvasElement
if (myElement instanceof HTMLCanvasElement) {
  const ctx = myElement.getContext('2d');
  console.log("Canvas context obtained!");
} else if (myElement) {
  console.warn("Element found, but it's not a canvas!", myElement.tagName);
} else {
  console.error("Element with ID 'myCanvas' not found!");
}

This approach is much safer because it handles all possible scenarios gracefully, preventing runtime crashes.

Pitfall 3: Not Enabling strictNullChecks (The Silent Killer)

This is perhaps one of the most common and insidious pitfalls, especially for those coming from JavaScript or older TypeScript configurations.

What is strictNullChecks? It’s a compiler option in your tsconfig.json. When enabled, TypeScript enforces that null and undefined are not assignable to types unless you explicitly include them. For example, string means string, not string | null | undefined. If you want to allow null or undefined, you must explicitly use a union type like string | null or string | undefined.

Why is it a Pitfall (when disabled)? When strictNullChecks is disabled (which it is by default in older projects or if you don’t explicitly enable strict: true), null and undefined are considered valid values for any type. This means TypeScript will happily let you write code that attempts to access properties or call methods on null or undefined values, leading to the infamous “Cannot read property ‘x’ of undefined” or “Cannot read property ‘x’ of null” runtime errors.

It’s like having a faulty seatbelt that sometimes doesn’t click into place, but you don’t get any warning until you’re in an accident.

The Solution: Always Enable strict: true in tsconfig.json The easiest and most effective way to enable strictNullChecks (along with many other beneficial strict checks) is to set "strict": true in your tsconfig.json.

Here’s how your tsconfig.json might look (using TypeScript 5.9.x, the latest stable as of December 2025):

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",                       /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "module": "Node18",                      /* Specify what module code is generated. */
    "rootDir": "./src",                      /* Specify the root folder within your source files. */
    "outDir": "./dist",                      /* Specify an output folder for all emitted files. */
    "esModuleInterop": true,                 /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
    "strict": true,                          /* Enable all strict type-checking options. */
    "skipLibCheck": true                     /* Skip type checking all .d.ts files. */
  },
  "include": ["src/**/*"],                   /* Specify files to include in compilation. */
  "exclude": ["node_modules", "dist"]        /* Specify files to exclude from compilation. */
}

The "strict": true flag is a powerhouse! It enables:

  • noImplicitAny: Prevents any from being implicitly inferred.
  • strictNullChecks: Prevents null and undefined from being assigned to non-nullable types.
  • strictFunctionTypes: Ensures function parameters are checked more strictly.
  • strictPropertyInitialization: Ensures class properties are initialized.
  • And more!

By enabling strict: true, you’re telling TypeScript to be your vigilant assistant, catching a huge class of common errors before they ever reach runtime. It might feel like a lot of initial errors, but think of them as gifts that save you debugging headaches later!


Step-by-Step Implementation: Fixing the Fault Lines

Let’s put these concepts into practice. We’ll start with some problematic code and then incrementally fix it using the best practices we just discussed.

First, make sure you have a project set up. If you’re following along from previous chapters, you should already have a tsconfig.json with strict: true enabled. If not, create a new directory, run npm init -y, and then install TypeScript:

mkdir ts-pitfalls-demo
cd ts-pitfalls-demo
npm init -y
npm install -D [email protected] # Or the latest stable version you find
npx tsc --init

(Note: As of December 5, 2025, TypeScript 5.9.3 is a highly plausible stable version based on search results. Always verify the absolute latest with npm view typescript version.)

Now, open your tsconfig.json and ensure "strict": true is uncommented and set. If you just ran npx tsc --init, it should be there by default.

Create a new file src/app.ts.

Example 1: any vs. unknown

Let’s start with a function that might receive data of various shapes.

Problematic Code (using any):

Add the following to src/app.ts:

// src/app.ts (Initial - Problematic)

// Let's imagine we're receiving data from an external API
// and we're not entirely sure of its shape.
function processIncomingDataAny(data: any) {
  console.log("Processing data (using any):");
  // We assume 'data' has a 'id' property which is a number
  // and a 'name' property which is a string.
  console.log(`ID: ${data.id}, Name: ${data.name.toUpperCase()}`);
}

console.log("--- Testing with 'any' ---");
processIncomingDataAny({ id: 123, name: "Alice" }); // Works
processIncomingDataAny({ id: 456, title: "Book" }); // Runtime error: data.name is undefined
processIncomingDataAny(null);                      // Runtime error: Cannot read properties of null (reading 'toUpperCase')

console.log("\n--- 'any' issues demonstrated ---");

Compile and run this. You’ll see the runtime errors:

npx tsc # Compile
node dist/app.js # Run

You’ll get output like:

--- Testing with 'any' ---
Processing data (using any):
ID: 123, Name: ALICE
Processing data (using any):
/home/user/ts-pitfalls-demo/dist/app.js:10
  console.log(`ID: ${data.id}, Name: ${data.name.toUpperCase()}`);
                                            ^

TypeError: Cannot read properties of undefined (reading 'toUpperCase')
    at processIncomingDataAny (/home/user/ts-pitfalls-demo/dist/app.js:10:45)
    at Object.<anonymous> (/home/user/ts-pitfalls-demo/dist/app.js:15:1)
    at Module._compile (node:internal/modules/cjs/loader:1376:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
    at Module.load (node:internal/modules/cjs/loader:1207:32)
    at Module._load (node:internal/modules/cjs/loader:1023:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:128:12)
    at node:internal/main/run_main_module:28:49

TypeScript didn’t catch these!

Solution (using unknown and Type Guards):

Now, let’s modify src/app.ts to use unknown and proper type guards. We’ll define an interface for the expected data shape.

// src/app.ts (Modified - Solution for any/unknown)

interface UserData {
  id: number;
  name: string;
}

// Helper function to check if an object matches our UserData interface
function isUserData(data: unknown): data is UserData {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    typeof (data as { id: unknown }).id === 'number' && // Assert for type narrowing check
    'name' in data &&
    typeof (data as { name: unknown }).name === 'string'
  );
}

function processIncomingDataUnknown(data: unknown) {
  console.log("\nProcessing data (using unknown):");
  if (isUserData(data)) {
    // Now TypeScript knows 'data' is UserData
    console.log(`ID: ${data.id}, Name: ${data.name.toUpperCase()}`);
  } else {
    console.warn("Received invalid user data format:", data);
  }
}

console.log("\n--- Testing with 'unknown' ---");
processIncomingDataUnknown({ id: 123, name: "Alice" }); // Works
processIncomingDataUnknown({ id: 456, title: "Book" }); // Safely caught
processIncomingDataUnknown(null);                      // Safely caught
processIncomingDataUnknown("just a string");           // Safely caught

console.log("\n--- 'unknown' issues resolved ---");

// Keep the previous problematic code commented out or remove it for clarity
/*
function processIncomingDataAny(data: any) {
  console.log("Processing data (using any):");
  console.log(`ID: ${data.id}, Name: ${data.name.toUpperCase()}`);
}
console.log("--- Testing with 'any' ---");
processIncomingDataAny({ id: 123, name: "Alice" });
processIncomingDataAny({ id: 456, title: "Book" });
processIncomingDataAny(null);
console.log("\n--- 'any' issues demonstrated ---");
*/

Compile and run again:

npx tsc
node dist/app.js

Now you’ll see:

--- Testing with 'unknown' ---
Processing data (using unknown):
ID: 123, Name: ALICE

Processing data (using unknown):
Received invalid user data format: { id: 456, title: 'Book' }

Processing data (using unknown):
Received invalid user data format: null

Processing data (using unknown):
Received invalid user data format: just a string

--- 'unknown' issues resolved ---

No runtime errors! TypeScript, combined with your isUserData type guard, now ensures safety.

Example 2: Type Assertions vs. Instanceof/Type Guards

Let’s simulate working with DOM elements where you might be tempted to use type assertions.

Add the following to src/app.ts, after the previous examples.

Problematic Code (using as assertion):

// src/app.ts (Problematic Type Assertion)

// Simulate a DOM environment
declare const document: {
  getElementById(id: string): HTMLElement | null;
};

function getButtonTextAssertion(elementId: string): string | null {
  const element = document.getElementById(elementId);

  // DANGER! We are asserting it's an HTMLButtonElement without verification.
  // What if it's a div, or null?
  const button = element as HTMLButtonElement; // TypeScript trusts us!

  // If 'element' was null, 'button' is null. If it was a div, 'button' is a div.
  // Accessing .textContent on null/div is a runtime error if 'strictNullChecks' wasn't active,
  // or just plain wrong if it's a div.
  return button?.textContent || null;
}

console.log("\n--- Testing problematic type assertion ---");
// Imagine these elements exist in an HTML file:
// <button id="myBtn">Click Me</button>
// <div id="myDiv">I am a Div</div>
// (no element with 'nonExistent')

// Simulate document.getElementById results:
// For 'myBtn': Returns an actual HTMLButtonElement
document.getElementById = (id: string) => {
  if (id === 'myBtn') return { textContent: "Click Me", tagName: "BUTTON" } as HTMLButtonElement;
  if (id === 'myDiv') return { textContent: "I am a Div", tagName: "DIV" } as HTMLDivElement;
  return null;
};

console.log("Button text (myBtn):", getButtonTextAssertion('myBtn'));
console.log("Button text (myDiv):", getButtonTextAssertion('myDiv')); // Compiles, but semantically wrong (it's a div)
console.log("Button text (nonExistent):", getButtonTextAssertion('nonExistent')); // Compiles, but `button` is null, `?.textContent` handles it, but still a weak assertion.

The console.log for myDiv will output “I am a Div”, which seems correct, but we’ve treated a div as a button without any real checks. This could lead to issues if we tried to call button-specific methods. For nonExistent, the ?. operator saves us from a null error, but the assertion was still too optimistic.

Solution (using Type Guards):

Now, let’s implement a safer version using type guards.

// src/app.ts (Solution for Type Assertion)

function getButtonTextSafe(elementId: string): string | null {
  const element = document.getElementById(elementId);

  // Check if element exists AND is an HTMLButtonElement
  if (element instanceof HTMLButtonElement) {
    // TypeScript now knows 'element' is HTMLButtonElement
    return element.textContent;
  } else if (element) {
    // Element exists but is not a button
    console.warn(`Element with ID '${elementId}' found, but it's a '${element.tagName}', not a button.`);
    return null;
  } else {
    // Element not found
    console.warn(`Element with ID '${elementId}' not found.`);
    return null;
  }
}

console.log("\n--- Testing safe type checking ---");
console.log("Button text (myBtn):", getButtonTextSafe('myBtn'));
console.log("Button text (myDiv):", getButtonTextSafe('myDiv'));
console.log("Button text (nonExistent):", getButtonTextSafe('nonExistent'));

Compile and run:

npx tsc
node dist/app.js

You’ll get output like:

--- Testing safe type checking ---
Button text (myBtn): Click Me
Element with ID 'myDiv' found, but it's a 'DIV', not a button.
Button text (myDiv): null
Element with ID 'nonExistent' not found.
Button text (nonExistent): null

This is much more robust! We clearly distinguish between a button, a non-button element, and a missing element.

Example 3: strictNullChecks in Action

You should already have strict: true in your tsconfig.json, which includes strictNullChecks. Let’s see how it helps.

Add the following to src/app.ts:

// src/app.ts (strictNullChecks demonstration)

interface UserProfile {
  name: string;
  email?: string; // Optional property, so it can be 'string | undefined'
  phoneNumber: string | null; // Explicitly allows null
}

function displayUserProfile(user: UserProfile) {
  console.log(`\n--- Displaying User Profile for ${user.name} ---`);
  console.log(`Name: ${user.name}`);

  // Without strictNullChecks, TypeScript wouldn't complain about email.toUpperCase() if email was undefined.
  // With strictNullChecks (enabled by "strict": true), TypeScript forces us to check.
  if (user.email) { // Type guard: checks if email is not undefined or null
    console.log(`Email: ${user.email.toLowerCase()}`); // Safe to call toLowerCase()
  } else {
    console.log("Email: Not provided.");
  }

  // Same for phoneNumber, which can be string or null
  if (user.phoneNumber) { // Type guard: checks if phoneNumber is not null
    console.log(`Phone: ${user.phoneNumber.replace(/\D/g, '')}`); // Safe to call replace()
  } else {
    console.log("Phone: Not available.");
  }
}

console.log("\n--- Testing strictNullChecks ---");

const user1: UserProfile = {
  name: "Bob",
  email: "[email protected]",
  phoneNumber: "123-456-7890"
};
displayUserProfile(user1);

const user2: UserProfile = {
  name: "Charlie",
  // email is optional, so we can omit it
  phoneNumber: null // Explicitly null
};
displayUserProfile(user2);

// Try to assign null to a non-nullable type (THIS WILL CAUSE A TS ERROR!)
// const invalidNameUser: UserProfile = {
//   name: null, // Error: Type 'null' is not assignable to type 'string'.
//   phoneNumber: "555-1212"
// };
// displayUserProfile(invalidNameUser);

If you try to uncomment the invalidNameUser block, TypeScript will immediately give you an error: Type 'null' is not assignable to type 'string'. This is strictNullChecks doing its job! It forces you to be explicit about null and undefined, making your code much safer.


Mini-Challenge: Refactoring for Robustness

Alright, your turn! You’ve seen the pitfalls and the solutions. Now, put on your TypeScript detective hat.

Challenge: You’ve inherited a small utility function that’s supposed to parse a string and return a user ID. It currently uses any and a potentially unsafe type assertion. Your task is to refactor it to be completely type-safe using unknown and proper type guards, preventing any possible runtime errors.

Here’s the problematic function:

// Mini-Challenge: Problematic function
function parseUserIdUnsafe(input: any): number | null {
  if (typeof input === 'string') {
    const parsed = parseInt(input, 10);
    // This assertion is risky if parsed isn't actually a number or is NaN
    return parsed as number;
  }
  return null;
}

console.log("\n--- Mini-Challenge: Unsafe parsing ---");
console.log("Parsed '123':", parseUserIdUnsafe('123'));
console.log("Parsed 'abc':", parseUserIdUnsafe('abc')); // Returns NaN, which is a number but not what we want
console.log("Parsed 456 (number):", parseUserIdUnsafe(456)); // Returns 456, but input was 'any'
console.log("Parsed null:", parseUserIdUnsafe(null)); // Returns null

Your Goal:

  1. Change the input parameter from any to unknown.
  2. Implement robust type guards to ensure input is a string before parsing.
  3. Add a check to ensure parseInt actually yields a valid number (not NaN).
  4. The function should return number | null, where null indicates failure to parse a valid ID.

Hint: Remember Number.isNaN() is your friend when checking the result of parseInt().

Take a moment, try it out in your src/app.ts file, and see if you can make it robust!

Click for Solution (but try it first!)
// Mini-Challenge: Solution
function parseUserIdSafe(input: unknown): number | null {
  console.log(`\nAttempting to parse: ${input}`);
  if (typeof input === 'string') {
    const parsed = parseInt(input, 10);
    // Check if the parsed result is a valid number (not NaN)
    if (!Number.isNaN(parsed)) {
      return parsed;
    } else {
      console.warn(`Could not parse string '${input}' into a valid number.`);
      return null;
    }
  } else {
    console.warn(`Input '${input}' is not a string, skipping parsing.`);
    return null;
  }
}

console.log("\n--- Mini-Challenge: Safe parsing solution ---");
console.log("Parsed '123':", parseUserIdSafe('123'));
console.log("Parsed 'abc':", parseUserIdSafe('abc'));
console.log("Parsed 456 (number):", parseUserIdSafe(456));
console.log("Parsed null:", parseUserIdSafe(null));
console.log("Parsed undefined:", parseUserIdSafe(undefined));

Common Pitfalls & Troubleshooting

Beyond the core pitfalls discussed, here are a few more common issues and how to approach them:

  1. Forgetting to Configure tsconfig.json for Your Environment:

    • Pitfall: Using default tsconfig.json settings (e.g., target: "ES5", module: "CommonJS") when your project uses modern Node.js or browser features. This can lead to unexpected transpilation or module resolution issues.
    • Solution: Always tailor your tsconfig.json compilerOptions to your target environment. For modern Node.js, target: "ES2022" (or higher) and module: "Node18" (or ESNext) are common. For browser apps, target: "ES2022" and module: "ESNext" with a bundler are typical. Refer to the official TypeScript documentation on tsconfig.json for the most current options.
    • Troubleshooting: If you see strange runtime errors related to import/require or features not working, double-check your target and module settings.
  2. Ignoring TypeScript Error Messages:

    • Pitfall: Seeing red squiggly lines or console errors during compilation (npx tsc) and trying to “hack around” them with // @ts-ignore or any without understanding the root cause.
    • Solution: TypeScript error messages are your friends! They provide incredibly valuable information about what went wrong and where. Take the time to read them carefully. Often, they even suggest solutions.
    • Troubleshooting: If an error message is cryptic, try pasting it into your search engine along with “TypeScript.” The official docs or community forums often have detailed explanations. Using your IDE’s quick-fix suggestions can also be a great learning tool.
  3. Over-complicating Types:

    • Pitfall: Sometimes, developers try to create overly complex types for every tiny variation, leading to types that are hard to read, maintain, and even harder for TypeScript to infer correctly.
    • Solution: Start simple. Let TypeScript infer types where it can. Use interfaces and types for clear contracts. Only introduce advanced generics, conditional types, or mapped types when absolutely necessary to achieve type safety for complex patterns. Prioritize readability.
    • Troubleshooting: If you find yourself struggling for hours to define a type, take a step back. Can you simplify the data structure? Can you break down the complex type into smaller, composable types? Sometimes, a slightly less “perfect” type that’s understandable is better than an unmaintainable, overly complex one.

Summary

Phew! We’ve covered some critical ground in this chapter. Understanding and actively avoiding common pitfalls is a huge step towards truly mastering TypeScript and writing robust, maintainable code.

Here are the key takeaways:

  • Avoid any like the plague: It’s an escape hatch that undermines TypeScript’s benefits. Opt for unknown instead.
  • Embrace unknown: Treat values of type unknown as sealed boxes. You must use type guards (like typeof, instanceof, or custom user-defined type guards) to narrow their type before you can interact with them safely.
  • Be cautious with Type Assertions (as Type): Only use them when you are absolutely, 100% certain about a type and the compiler can’t infer it. Otherwise, prefer runtime type guards for safety.
  • Always enable strict: true in tsconfig.json: This single setting activates a suite of strict checks, including strictNullChecks, noImplicitAny, and more, which will catch a vast majority of common errors early. It’s a non-negotiable best practice for modern TypeScript development (and has been since 2017!).
  • Don’t ignore compiler errors: They are helpful guides! Read them, understand them, and fix the underlying issues rather than silencing them.
  • Configure your tsconfig.json appropriately: Match your target and module options to your project’s runtime environment.

By proactively addressing these common pitfalls, you’re not just fixing errors; you’re building a stronger foundation for all your future TypeScript projects. You’re transforming from a TypeScript user into a TypeScript craftsman!

In our next chapter, we’ll shift gears and dive into some truly advanced TypeScript design patterns. We’ll explore how to leverage TypeScript’s powerful type system to create highly flexible, scalable, and maintainable application architectures. Get ready to unlock the next level of type wizardry!