When a developer who had spent four years writing Python backend services decided to switch to a TypeScript-first team, she expected the type system to be the main learning curve. She was wrong. The type system was fine after the first two weeks. What took most of the five weeks was something less obvious: understanding when TypeScript's structural type system is protecting you and when it's letting you think you're safe when you're not.
We tracked 200 PRs across her five-week switch. What we found confirmed a pattern we've seen before: the concepts that cause the most persistent problems in TypeScript are not the ones that look hard on the surface.
Week one and two: the expected surface
The first two weeks produced exactly what you'd expect. Type annotation gaps, missing return types on functions, Python-style duck typing applied incorrectly to TypeScript interfaces. A few any casts in places where the developer wasn't sure how to express the type yet.
These are easy to give feedback on. The concepts behind them, structural typing and explicit type annotation, are teachable and the feedback produces visible learning. By the end of week two, the surface errors were mostly gone. Type annotations were present. The any casts were largely cleaned up.
At that point, we expected to start seeing deeper issues. We did, but they weren't the ones we would have predicted.
The persistent gap: type narrowing under control flow
TypeScript's type narrowing system is one of its most powerful features and also one of the most easy to underuse. When you write an if (user !== null) check, TypeScript narrows the type of user inside that block. When you write an if (typeof value === 'string') check, TypeScript narrows inside that block too.
Developers coming from Python understand null checks and type guards conceptually. What they don't instinctively reach for is TypeScript's discriminated union pattern, where a shared literal property on a union type lets you narrow cleanly to a specific member. In Python, a similar pattern would be handled with isinstance checks or by inspecting a shared attribute.
In TypeScript, the idiomatic way to write this creates a better type-checking guarantee at each branch. But it requires declaring the union with the discriminant explicitly, which feels verbose compared to how Python handles the same problem. The developer's PRs showed this pattern consistently: union types were used, but without discriminants, so the type narrowing inside branches was less precise than it could be.
This persisted through week three and into week four, which was longer than the error annotation issues had taken to resolve. The reason is that the gap here wasn't syntax, it was design instinct. TypeScript discriminated unions are a pattern choice, not a syntax requirement. The code compiled and ran correctly without them.
Promise handling and async mental models
The second persistent category was async/await and Promise chaining. Python's asyncio and TypeScript's Promises are similar enough that the surface looks familiar, but the TypeScript ecosystem patterns for error handling in async code differ enough to cause problems.
Python asyncio code typically wraps async calls in try-except blocks to catch exceptions. TypeScript Promise chains use .catch() or try-catch around await expressions, and the patterns mix in ways that can produce unhandled rejections that are hard to trace. The developer's PRs frequently mixed .then()/.catch() chains with await in the same function, which technically works but makes the control flow harder to read and produces inconsistent error handling behavior depending on which style is in effect at which point.
The TypeScript convention is to pick one style per function and be consistent. The developer needed to see this feedback several times, in different PRs, before it became instinctive. That's normal for a pattern-level change rather than a syntax-level change.
What resolved quickly, what did not
Concepts that resolved within one or two PRs after feedback: return type annotation, interface versus type alias usage, basic generics syntax, enum usage.
Concepts that took three or more PRs to resolve after initial feedback: discriminated unions, consistent async style, type guard functions for narrowing non-primitive types.
The split is instructive. The quick-resolving concepts are largely syntactic: once you've seen the correct form, you write it. The slow-resolving concepts require a change in design instinct: they require thinking differently about how to structure code, not just how to annotate it.
Where Python experience helped
This piece has focused on the gaps, which is where the learning work was. But it's worth naming where Python experience was actively useful in the TypeScript switch.
Data transformation code came naturally. The developer wrote clean, readable TypeScript array operations from day one, because Python's list comprehension instincts map reasonably well to TypeScript's .map()/.filter()/.reduce() chains. Function decomposition and naming were strong. Variable names were meaningful. The code was readable to a TypeScript developer even when it wasn't idiomatic.
What experienced developers bring to a stack switch is not nothing. The quality of thought in the code was high from the start. What was missing were the TypeScript-specific design patterns that take time to internalize, not because they're hard, but because they require overwriting an existing reflex with a new one.
Five weeks to reach a point where PRs passed review without significant feedback on idiomatic patterns is a reasonable timeline for a switch at that experience level. The concept categories that took longest were specific and consistent. That specificity is what makes targeted feedback more useful than a general TypeScript course: the course covers everything, but the developer needed help on two things, not everything.