prefer-type-annotations
Prefer Type Annotations to Type Assertions
Overview
Type annotations (: Type) verify that values conform to types. Type assertions (as Type) tell TypeScript to trust you.
Annotations check your work. Assertions bypass checks. When you have a choice, prefer annotations.
When to Use This Skill
- Assigning values to variables
- Defining function return types
- Working with object literals
- Tempted to use
as Typeto fix errors - Getting "not assignable" errors
The Iron Rule
NEVER use type assertions when type annotations would work.
No exceptions:
- Not for "it's obviously correct"
- Not for "I know better than TypeScript"
- Not for "the assertion is simpler"
Detection: The "as" Smell
See as SomeType? Ask: "Could I use a type annotation instead?"
// ❌ VIOLATION: Type assertion bypasses checking
const alice = {} as Person; // No error! But alice has no properties
// ✅ CORRECT: Type annotation verifies the value
const alice: Person = {};
// ~~~~~ Property 'name' is missing in type '{}' but required in type 'Person'
The Critical Difference
Type Annotation (Safe)
interface Person { name: string }
// TypeScript CHECKS that the value matches the type
const bob: Person = { name: 'Bob' }; // OK
const bad: Person = {}; // Error: missing 'name'
const extra: Person = {
name: 'Carol',
age: 30 // Error: 'age' does not exist on type 'Person'
};
Type Assertion (Unsafe)
// TypeScript TRUSTS you that the value matches the type
const bob = {} as Person; // No error, but wrong!
const bad = { foo: 1 } as Person; // No error, but wrong!
Arrow Functions: Annotate Returns
// ❌ VIOLATION: Assertion hides errors
const people = ['alice', 'bob'].map(name => ({name} as Person));
// Even worse: completely wrong values slip through
const people = ['alice', 'bob'].map(name => ({} as Person)); // No error!
// ✅ CORRECT: Annotate the return type
const people = ['alice', 'bob'].map((name): Person => ({name})); // OK
const people = ['alice', 'bob'].map((name): Person => ({}));
// ~~ Error: missing 'name'
When Assertions ARE Appropriate
Type assertions make sense when you truly know more than TypeScript:
1. DOM Elements
// You know #myButton exists and is a button
const button = document.querySelector('#myButton') as HTMLButtonElement;
// Better: include a comment explaining why
const button = document.querySelector('#myButton') as HTMLButtonElement;
// This button is created in index.html and always exists
2. After Runtime Checks
const el = document.getElementById('foo');
if (el) {
el.innerHTML = 'Hello'; // TypeScript knows el is not null
}
// Or with non-null assertion (use sparingly!)
const el = document.getElementById('foo')!;
Pressure Resistance Protocol
1. "The Error Is Wrong"
Pressure: "TypeScript is complaining but my code is correct"
Response: TypeScript is usually right. Read the error message carefully.
Action:
- Check if your type definition matches your intention
- Check if your value actually matches the type
- Only use assertion if you can explain WHY TypeScript is wrong
2. "The Assertion Is Simpler"
Pressure: "Adding annotations everywhere is verbose"
Response: Annotations catch bugs. Assertions hide them. Safety > brevity.
Action: Add the annotation. Your future self will thank you.
3. "I Know The Type At Runtime"
Pressure: "I checked the type at runtime, so assertion is safe"
Response: If you checked at runtime, TypeScript should be able to narrow the type.
Action: Use a type guard or conditional to narrow, not an assertion.
Red Flags - STOP and Reconsider
as anyanywhere in your codeas Typeimmediately after creating an object- Multiple assertions in a chain (
x as A as B) - Assertions to fix "not assignable" errors
!(non-null assertion) without good reason
Common Rationalizations (All Invalid)
| Excuse | Reality |
|---|---|
| "I know it's a Person" | Then prove it with an annotation, not an assertion. |
| "TypeScript is wrong" | TypeScript found a real inconsistency. Investigate. |
| "It's just one assertion" | Assertions spread. One leads to many. |
| "The types are too strict" | Strict types catch bugs. Embrace them. |
Quick Reference
| Situation | Use Annotation | Use Assertion |
|---|---|---|
| Variable declaration | :Type |
Never |
| Function parameter | :Type |
Never |
| Function return | :Type |
Rarely |
| Object literal | :Type |
Never |
| DOM element you know | - | as HTMLElement with comment |
| After null check | - | ! if certain |
| Unknown value from API | - | After validation with unknown |
The Non-Null Assertion (!)
// Tells TypeScript: "Trust me, this isn't null"
const el = document.getElementById('foo')!;
// Prefer a runtime check:
const el = document.getElementById('foo');
if (!el) throw new Error('No element #foo');
// Now TypeScript knows el is not null
The Bottom Line
Type annotations are a contract. Type assertions are a lie you tell TypeScript.
Use annotations to declare your intentions. Use assertions only when you have information TypeScript cannot have. Always include a comment explaining why an assertion is valid.
Reference
Based on "Effective TypeScript" by Dan Vanderkam, Item 9: Prefer Type Annotations to Type Assertions.
More from marius-townhouse/effective-typescript-skills
exhaustiveness-checking
Use when handling tagged unions. Use when adding new cases to discriminated unions. Use when switch statements must cover all cases.
13precise-string-types
Use when working with string-typed properties. Use when string values have a limited set of options. Use when keyof could provide better type safety.
10codegen-over-complex-types
Use when types become extremely complex. Use when types mirror external schemas. Use when maintaining type-to-schema mappings. Use when types require extensive type-level logic. Use when types drift from data sources.
10limit-optional-properties
Use when adding optional properties. Use when types have many optional fields. Use when considering required vs optional.
10imprecise-over-inaccurate
Use when types become too complex. Use when precision causes false positives. Use when accuracy is uncertain.
10ts-check-jsdoc-experiment
Use when experimenting with TypeScript. Use when migrating JavaScript gradually. Use when adding types to JS files. Use when teams are learning TypeScript. Use when validating JavaScript with types.
10