TypeScript 12 🔷 Type Assertions and Type Casting
Sometimes you know more about a value’s type than TypeScript does. A DOM query returns HTMLElement | null, but you know the element is an <input>. An API response parses to any or unknown, but you know its shape. A union needs to be used as a specific branch. In these cases, TypeScript gives you type assertions — the as operator and its older cousin, the angle-bracket syntax. They tell the compiler “trust me, this value is this type.” They don’t change the value at runtime — they change what the compiler thinks the value is. Used well, they bridge the gap between dynamic inputs and static types. Used badly, they erase the safety the type system exists to provide.
Key point: A type assertion is not a cast. In C, Java, or C#, a cast converts a value at runtime — it may allocate, box, unbox, or throw. TypeScript’s as does nothing at runtime. It’s a compile-time claim: “I know this better than you.” If your claim is wrong, the code crashes at runtime exactly where it would in plain JavaScript — with no help from the type system. Assertions are a tool for the edges of your program, not a habit for the middle.
The two assertion syntaxes
TypeScript has two syntaxes for type assertions.
as syntax — the modern, preferred form:
const el = document.getElementById('app') as HTMLElement;
const value = someUnknown as string;
const config = JSON.parse(raw) as Config;
Angle-bracket syntax — the older form:
const el = <HTMLElement>document.getElementById('app');
const value = <string>someUnknown;
Both do exactly the same thing. But the angle-bracket form conflicts with JSX (.tsx files), so as is the standard in React/Angular/Vue code and the recommendation everywhere else. In .tsx files, <T>value is parsed as JSX, so as is required.
What an assertion doesn’t do:
- It doesn’t convert the value at runtime
- It doesn’t check that the value actually matches
- It doesn’t run any code — the assertion disappears at compile time
const n = '42' as unknown as number; // compiles, but n is still '42'
console.log(typeof n); // 'string'
That’s the trap. TypeScript says n is a number, but at runtime it’s a string. The assertion was a lie, and the compiler trusted it.
Why
asreplaced angle brackets: The<T>valueform conflicts with JSX. In.tsxfiles,<Foo>is a JSX element, not a cast. Rather than require different syntax per file type, TypeScript addedasand made it the preferred form.asreads more like a cast anyway —value as Typereads left-to-right, the way you think about it.
Assertions vs conversions
The word “cast” causes confusion because in most languages a cast does work at runtime. In TypeScript, it doesn’t.
| Language | Cast does | TypeScript as does |
|---|---|---|
| C | May reinterpret bits | Nothing at runtime |
| Java | May check, box, throw | Nothing at runtime |
| C# | May unbox, convert | Nothing at runtime |
Python (int(x)) | Calls a function | N/A |
TypeScript (as) | — | Tells compiler the type |
There is no runtime conversion. If you need an actual conversion, call a conversion function.
// Wrong — no runtime conversion
const n = '42' as number; // compiles, n is still the string '42'
// Right — actual conversion
const n = Number('42'); // number at runtime
const s = String(42); // string at runtime
const b = Boolean(0); // false at runtime
Practical implication: If you have a string and need a number, use Number(), parseInt(), or parseFloat(). If you have an unknown and need an object, validate it. The as operator is for cases where the value already is the type — you just need to tell the compiler.
Why the distinction matters: Every “I cast it and it still didn’t work” bug comes from confusing assertion with conversion. The assertion changed the type label. The value didn’t change. TypeScript trusted you. Runtime didn’t care. Learn this once, and 90% of
asconfusion disappears.
Basic assertions
Asserting between related types is allowed.
// A union branch
const value: string | number = getValue();
const s = value as string; // ✅ narrowing the compiler's view
// A DOM element
const el = document.getElementById('x') as HTMLInputElement;
// A parsed JSON value
const config = JSON.parse(raw) as Config;
// An unknown value
function handle(x: unknown): void {
const n = x as number; // ✅ compiler accepts
// but you're responsible for the check
}
Widening — allowed but useless:
const s = 'hello';
const wide = s as string; // ✅ allowed, no narrowing
Widening never fails, because 'hello' is already a string. It just throws away information.
Narrowing — the common case:
const value: unknown = 42;
const n = value as number; // ✅
Narrowing from unknown or any is almost always allowed — those types can become anything.
Narrowing between specific types — restricted:
const s = 'hello' as number; // ❌ string to number — not allowed
TypeScript rejects assertions between types it considers “sufficiently different.” You can override with as unknown as T, but that’s a red flag.
The rule: An assertion is allowed if either type is assignable to the other. unknown is assignable to everything, so unknown as T works. 'hello' and number are unrelated, so the assertion is rejected.
Why narrowing is free but unrelated types aren’t: Asserting
unknown as numberis safe —unknownmight be a number. Asserting'hello' as numberis a lie — the value is definitely a string. TypeScript can’t stop you from lying entirely, but it refuses the obviously impossible cases. If you need to override that refusal,as unknown as Tsays “I know this is wrong, and I accept the risk.”
as unknown as T — the double assertion
When two types aren’t directly related, TypeScript rejects the assertion. You can force it with unknown in between.
const s = 'hello' as unknown as number; // ✅ compiles
This is a double assertion. It tells TypeScript: “Treat the string as unknown, then as a number.” Each step is legal, so the whole chain compiles.
When it’s justified: Never, usually. If you’re reaching for as unknown as T, something upstream is wrong — a badly typed library, a missing generic, a poorly modeled API. Fix the source instead.
When it’s the least-bad option:
- Third-party library types are wrong and you can’t fix them
- Migrating legacy code where types are gradually being introduced
- Test mocking where the runtime behavior is well-understood
Better alternatives:
- Fix the type at its source
- Use a wrapper function with proper types
- Validate the value and narrow
- Type the third-party module with a
.d.tsfile
// Bad
const result = (lib as any).doThing() as unknown as Result;
// Better — a typed wrapper
import * as lib from 'lib';
function doThing(): Result {
return (lib as { doThing: () => Result }).doThing();
}
Why
as unknown as Tis a red flag: It’s an explicit bypass of the type system. The compiler was telling you the assertion doesn’t make sense, and you overrode it. Sometimes necessary, but always a signal to look upstream. When you see one in code review, ask: “Why don’t the types line up here?” The answer is usually a better fix than the assertion.
When assertions are appropriate
There’s a legitimate list of cases.
1. DOM queries — TypeScript can’t know the element kind:
const input = document.getElementById('email') as HTMLInputElement;
input.value = 'x';
getElementById returns HTMLElement | null. You know it’s an <input> because of the markup. Assert to access .value.
2. JSON parsing — the result is any:
interface Config { port: number; host: string; }
const config = JSON.parse(raw) as Config;
JSON.parse returns any. Asserting gives you a typed value — with the understanding that you’re trusting the JSON. In production, validate with a runtime check (see Chapter on validation).
3. Library types are wrong or missing:
const result = (legacyLib.getData() as unknown) as ActualType;
Only when there’s no other option.
4. Narrowing a union for a specific branch:
type Message = { kind: 'text'; value: string } | { kind: 'image'; url: string };
function render(m: Message): void {
if (m.kind === 'text') {
// m narrowed here — no assertion needed
}
}
Actually, this is narrowing, not assertion — better, and always preferable.
5. event.target in DOM handlers:
function onClick(e: MouseEvent): void {
const button = e.target as HTMLButtonElement;
button.disabled = true;
}
event.target is EventTarget | null. The listener is on a button, so the target is a button — but the compiler can’t know.
6. Type-narrowing helpers that the compiler can’t prove:
function isUser(value: unknown): value is User {
return typeof value === 'object' && value !== null && 'id' in value;
}
That’s a type predicate, not an assertion — better than as because it’s checked at the boundary.
7. Symbol.iterator and library interop:
Rare, but sometimes libraries expect specific nominal types.
The pattern across all cases: Assertions are for the boundary between typed code and untyped reality — DOM, JSON, third-party libraries, user input. Inside your typed code, assertions are a sign something is modeled wrong.
Why boundary assertions are acceptable: TypeScript can’t know what HTML produced a DOM element, what shape a JSON payload has, or how a legacy library behaves. Those facts live outside the type system. Assertions let you import those facts into TypeScript — and place the trust boundary at the edge of your program rather than throughout it. The rule of thumb: assert once at the boundary, then stay typed inside.
Assertions vs type predicates
A type predicate is a function that returns value is T. It’s safer than as because the check is written down and runs at runtime.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function handle(x: unknown): void {
if (isString(x)) {
x.toUpperCase(); // ✅ x is string — checked at runtime
}
}
The difference:
asis a claim — no runtime check, no verificationis Tis a check — the function returns a boolean, and the compiler trusts the annotation when it’s true
Which to use: Always prefer is T when possible. Use as only when no runtime check is available (DOM, JSON.parse, library interop).
// Good — runtime check
if (isUser(data)) { data.name; }
// Risky — no runtime check
const user = data as User;
user.name; // crashes if data isn't a User
When is T is impossible: When the check is trivial and the compiler knows. When the value is already validated upstream. When writing a library that receives already-typed arguments.
Practical guidance: At every boundary, convert as to a type predicate when feasible.
// Before — trusts blindly
const user = JSON.parse(raw) as User;
// After — validates
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null
&& 'id' in x && typeof (x as any).id === 'number'
&& 'name' in x && typeof (x as any).name === 'string';
}
const parsed: unknown = JSON.parse(raw);
if (!isUser(parsed)) throw new Error('Invalid user');
// parsed is User from here
The predicate version is longer but catches bad data at runtime. The as version is shorter and crashes later.
Why predicates win: They’re checked at runtime.
asis checked at compile time only — by you, not the compiler. In a world where external data can be anything, a runtime check is strictly better. Predicates turn “I hope this is a User” into “this is a User, and I proved it.” That’s the difference between a claim and a proof.
Non-null assertion — !
The non-null assertion is a special case: value! asserts that value is not null or undefined.
const el = document.getElementById('app')!;
el.innerHTML = 'Hello';
TypeScript removes null and undefined from the type. No runtime check.
When it’s tempting:
function process(users: User[]): void {
const first = users[0]!;
first.name; // ✅ compiles, crashes if users is empty
}
When it’s safe:
- When you’ve just checked —
if (!users.length) return; const first = users[0]!; - When the value’s presence is guaranteed by the framework (Angular
@ViewChildafter init) - When a null check exists in a place the compiler can’t see
When it’s a bug:
- Accessing array elements without checking length
- Reading values from maps without checking presence
- Anywhere you’d be surprised if the value were
undefined
Better alternatives:
// Instead of !
if (users.length > 0) {
const first = users[0]; // typed as User (with noUncheckedIndexedAccess off)
}
// Or with optional chaining + fallback
const name = users[0]?.name ?? 'Anonymous';
noUncheckedIndexedAccess: Enabling this compiler flag makes users[0] typed as User | undefined. Then ! is explicit — and needed everywhere the compiler can’t prove the index is valid.
Why ! is dangerous: Every non-null assertion is a silent promise that the value isn’t null. When the promise is broken, the code crashes at runtime with a Cannot read property of undefined — exactly the error TypeScript’s strict null checks exist to prevent. Use ! when you’re certain, and fix the type when you’re not.
Why
!exists at all: Some patterns have a guaranteed non-null state the compiler can’t infer — a framework-managed reference, a validated-then-used value, a map key known to exist. Without!, you’d need redundant null checks everywhere. With it, you assert once. But the assertion should be rare. If you’re writing!every other line, the types are wrong somewhere.
Assertions on object literals — satisfies instead
satisfies is a newer operator that checks an expression against a type without changing its type. It’s often the right answer when you’re tempted to use as.
const config = {
apiUrl: 'https://x',
timeout: 5000
} satisfies Config;
// config is still { apiUrl: string; timeout: number } — the literal type
Compare with as:
const config = {
apiUrl: 'https://x',
timeout: 5000
} as Config;
// config is Config — the named type
The difference:
asreplaces the typesatisfieschecks the type and keeps the narrower inferred type
When satisfies is better:
const STATUS = {
Idle: 'idle',
Ready: 'ready'
} satisfies Record<string, string>;
// typeof STATUS.Idle is 'idle' — literal preserved
With as, the literal types would be lost:
const STATUS = {
Idle: 'idle',
Ready: 'ready'
} as Record<string, string>;
// typeof STATUS.Idle is string — literal lost
satisfies gives you the check without the loss of precision.
Comparison:
| Operator | Checks the type | Changes the type | Literal types preserved |
|---|---|---|---|
as | No | Yes | No |
satisfies | Yes | No | Yes |
: Type annotation | Yes | Yes | No |
When you’d use as over satisfies: Rarely — mostly when you want to widen a value’s type deliberately, or when the value might be a different type at runtime and you’re claiming a specific one (interop).
The modern rule: Reach for satisfies first. It’s strictly safer than as because it checks.
Why
satisfieswas added:asis a blunt instrument — it changes the type without checking. People used it to validate config objects against a type, but lost the literal types.satisfieswas added in TypeScript 4.9 to fill the gap: check the type, keep the inferred narrow type. For configuration, schema validation, and const tables, it’s usually what you want.
A full example
A small boundary-handling module that uses assertions and predicates correctly.
// ============================================
// DOM — ASSERTION IS REASONABLE
// ============================================
const form = document.getElementById('signup-form') as HTMLFormElement;
const emailInput = document.getElementById('email') as HTMLInputElement;
// ============================================
// JSON — VALIDATE WITH A PREDICATE
// ============================================
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof (value as { id: unknown }).id === 'number' &&
'name' in value &&
typeof (value as { name: unknown }).name === 'string' &&
'email' in value &&
typeof (value as { email: unknown }).email === 'string'
);
}
function parseUser(raw: string): User {
const parsed: unknown = JSON.parse(raw);
if (!isUser(parsed)) {
throw new Error('Invalid user payload');
}
return parsed; // narrowed to User
}
// ============================================
// EVENT TARGET — ASSERTION AFTER CONTEXT
// ============================================
function onClick(e: MouseEvent): void {
const button = e.target as HTMLButtonElement;
console.log(button.disabled);
}
// ============================================
// SATISFIES — CHECK WITHOUT WIDENING
// ============================================
const ROUTES = {
home: '/',
about: '/about',
user: '/users/:id'
} satisfies Record<string, string>;
type RouteName = keyof typeof ROUTES; // 'home' | 'about' | 'user'
const home = ROUTES.home; // '/' (literal preserved)
// ============================================
// USAGE
// ============================================
const raw = '{"id":1,"name":"Alice","email":"alice@example.com"}';
const user = parseUser(raw);
console.log(user.name); // Alice
console.log(form.tagName); // FORM
console.log(emailInput.type); // email
console.log(ROUTES, home);
What’s asserted: DOM elements (with a comment explaining why), event targets (in context).
What’s validated: The JSON payload — with a predicate, not an assertion.
What’s checked and preserved: The ROUTES object, via satisfies.
That’s the pattern at scale: assert at the edges, validate untrusted input, use satisfies when you want the check without widening.
Why this mix: DOM elements and event targets are cases where the type system can’t know the specific element — an assertion is the tool. External data is untrusted — a predicate is the tool. Constant tables need both a check and preserved literals —
satisfiesis the tool. Assertions aren’t banned; they’re just one option among several, and usually not the first.
Complete Example Session
# ============================================
# PART 1: BASIC ASSERTIONS
# ============================================
cat > basic.ts << 'EOF'
const value: unknown = 'hello';
const s = value as string;
console.log(s.toUpperCase()); // HELLO
const num = 42;
const n = num as number; // widening — no-op
console.log(n);
// Double assertion
const forced = 'hello' as unknown as number;
console.log(typeof forced); // 'string' — assertion doesn't convert
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: WHAT ASSERTION DOES NOT DO
# ============================================
cat > no-conversion.ts << 'EOF'
const s = '42';
const n = s as unknown as number;
console.log(typeof s, typeof n); // both 'string'
console.log(n + 1); // '421' — string concat, not arithmetic
// Correct conversion
const realNum = Number(s);
console.log(realNum + 1); // 43
EOF
npx tsc --noEmit no-conversion.ts
# (no errors)
# ============================================
# PART 3: DOM AND JSON
# ============================================
cat > dom.ts << 'EOF'
interface User {
id: number;
name: string;
}
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null
&& typeof (x as { id?: unknown }).id === 'number'
&& typeof (x as { name?: unknown }).name === 'string';
}
const raw = '{"id":1,"name":"Alice"}';
const parsed: unknown = JSON.parse(raw);
if (!isUser(parsed)) throw new Error('bad');
console.log(parsed.name);
EOF
npx tsc --noEmit dom.ts
# (no errors)
# ============================================
# PART 4: NON-NULL ASSERTION
# ============================================
cat > nonnull.ts << 'EOF'
function firstOrThrow<T>(xs: T[]): T {
if (xs.length === 0) throw new Error('empty');
return xs[0]!; // safe — length checked
}
console.log(firstOrThrow([1, 2, 3]));
// Better without ! — narrowing
function firstOr<T>(xs: T[], fallback: T): T {
return xs.length > 0 ? xs[0] : fallback;
}
console.log(firstOr([], 'none'));
EOF
npx tsc --noEmit nonnull.ts
# (no errors)
# ============================================
# PART 5: SATISFIES
# ============================================
cat > satisfies.ts << 'EOF'
const ROUTES = {
home: '/',
about: '/about'
} satisfies Record<string, string>;
type RouteName = keyof typeof ROUTES;
const r: RouteName = 'home';
// Literal preserved
const home = ROUTES.home; // type: '/'
// Would fail if a value weren't a string
// const bad = { home: 123 } satisfies Record<string, string>;
console.log(r, home);
EOF
npx tsc --noEmit satisfies.ts
# (no errors)
# ============================================
# PART 6: ASSERTION WITH CHECK
# ============================================
cat > guarded.ts << 'EOF'
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'square': return s.side ** 2;
}
}
// Predicate
function isCircle(s: Shape): s is { kind: 'circle'; radius: number } {
return s.kind === 'circle';
}
const s: Shape = { kind: 'circle', radius: 1 };
if (isCircle(s)) {
console.log(s.radius);
}
EOF
npx tsc --noEmit guarded.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basic.ts no-conversion.ts dom.ts nonnull.ts satisfies.ts guarded.ts
node basic.js
# [ HELLO ]
# [ 42 ]
# [ string ]
node no-conversion.js
# [ string string ]
# [ 421 ]
# [ 43 ]
node dom.js
# [ Alice ]
node nonnull.js
# [ 1 ]
# [ none ]
node satisfies.js
# [ home / ]
node guarded.js
# [ 1 ]
Quick Reference
Assertion Syntax
| Form | Example | Notes |
|---|---|---|
as | x as T | Preferred |
| Angle brackets | <T>x | Conflicts with JSX |
| Non-null | x! | Removes null/undefined |
| Double | x as unknown as T | Escape hatch |
What Assertions Do
| Action | Performed |
|---|---|
| Change the compile-time type | ✅ |
| Change the runtime value | ❌ |
| Check the runtime value | ❌ |
| Emit JavaScript | ❌ (erased) |
| Fail at runtime | ❌ (crash if wrong) |
Assertion vs Conversion
| Need | Tool |
|---|---|
| Change the type | as |
| Change the value | Number(), String(), Boolean() |
| Parse a number | parseInt, parseFloat, Number() |
| Convert to string | String(), .toString() |
| Convert to boolean | Boolean(), !! |
Assertion Allowance Rules
| From | To | Allowed |
|---|---|---|
unknown | T | ✅ |
any | T | ✅ |
T | unknown | ✅ |
T | any | ✅ |
Union A | B | A | ✅ |
string | number | ❌ |
| Unrelated types | — | ❌ (need as unknown as T) |
Non-Null Assertion !
| Use | Safe |
|---|---|
| After length check | ✅ |
| After null check | ✅ |
| Framework-managed refs | ✅ |
| Without any check | ❌ |
| On array index without length check | ❌ |
| On map get without has check | ❌ |
as vs satisfies
| Aspect | as T | satisfies T |
|---|---|---|
| Checks the type | ❌ | ✅ |
| Changes the type | ✅ to T | ❌ keeps inferred |
| Preserves literals | ❌ | ✅ |
| Runtime effect | None | None |
| Use for config | ⚠️ if you want T | ✅ if you want check + literals |
as vs is T
| Aspect | as T | is T |
|---|---|---|
| Runtime check | ❌ | ✅ |
| Compile-time only | ✅ | ❌ |
| Where used | Anywhere | Function return |
| Safety | Trust | Proof |
| Preferred at boundaries | Rarely | ✅ |
Common Assertion Targets
| Value | Assertion |
|---|---|
document.getElementById | as HTMLElement |
document.getElementById | as HTMLInputElement |
event.target | as HTMLButtonElement |
JSON.parse | as Config |
unknown | as T |
null | T | ! |
Assertion Red Flags
| Pattern | Signal |
|---|---|
as unknown as T | Something upstream is wrong |
as any | Bypassing the type system |
Multiple as in a row | Layers of trust |
as T everywhere | Types are modeled wrong |
! on array index without check | Potential crash |
Best Practices
✅ Do This:
// Assert at boundaries — DOM
const el = document.getElementById('x') as HTMLInputElement; // ✅
// Validate external data
function isUser(x: unknown): x is User { ... } // ✅
// Use `satisfies` for config objects
const ROUTES = { home: '/' } satisfies Record<string, string>; // ✅
// Use `!` after a check
if (xs.length === 0) return;
const first = xs[0]!; // ✅
// Convert values, don't assert them
const n = Number('42'); // ✅
// Use `unknown` for untrusted input
function handle(x: unknown): void { } // ✅
// Use type predicates for narrowing
function isString(x: unknown): x is string { return typeof x === 'string'; } // ✅
// Write a comment when asserting
const form = document.getElementById('f') as HTMLFormElement; // ✅ know it's a form
❌ Don’t Do This:
// Don't use `as` to convert runtime values
const n = '42' as number; // ❌ n is still a string
// Don't assert on untrusted data
const user = JSON.parse(raw) as User; // ⚠️ no validation
// Don't use `as unknown as T` casually
const x = 'a' as unknown as number; // ❌ double lie
// Don't use `!` without checking
const first = xs[0]!; // ⚠️ crashes if empty
// Don't use `as any`
const y = data as any; // ❌ erases types
// Don't assert in the middle of typed code
const sum = (a as number) + (b as number); // ⚠️ types should flow
// Don't confuse `as` with `satisfies`
const cfg = { port: 8080 } as Record<string, number>; // ⚠️ use satisfies
// Don't use `as` for narrow types you can infer
const n = 42 as number; // ⚠️ redundant
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
as ≠ conversion | Value unchanged | Use Number() etc. |
| Asserting on untrusted data | No runtime check | Use a predicate |
as unknown as T proliferation | Hidden type errors | Fix upstream |
! without a check | Runtime crash | Narrow first |
| Asserting in the middle of code | Types out of sync | Annotate properly |
as any | Erases types | Use unknown |
Confusing as and satisfies | Lose literals | satisfies for checks |
| Asserting an element as the wrong type | Runtime error | Match the actual element |
<T> in .tsx | JSX conflict | Use as T |
| Overriding union narrowing | Wrong branch | Use is predicates |
Real-World Examples
1. DOM element
const input = document.getElementById('email') as HTMLInputElement;
2. Form element
const form = document.getElementById('signup') as HTMLFormElement;
3. Event target
const button = e.target as HTMLButtonElement;
4. JSON parse
const config = JSON.parse(raw) as Config;
5. JSON parse with validation
if (!isConfig(JSON.parse(raw))) throw new Error('bad config');
6. Non-null assertion after check
if (xs.length > 0) { const first = xs[0]!; }
7. unknown to specific
const n = value as number;
8. unknown from any
function handle(x: unknown): void { }
9. Type predicate
function isString(x: unknown): x is string { return typeof x === 'string'; }
10. satisfies for config
const ROUTES = { home: '/' } satisfies Record<string, string>;
11. Literal preserved with satisfies
const ROUTES = { home: '/' } as const satisfies Record<string, string>;
12. Double assertion (rare)
const x = someValue as unknown as SpecificType;
13. Asserting on legacy lib
const result = (legacy.getData() as unknown) as ActualType;
14. Number conversion
const n = Number('42');
15. Boolean conversion
const b = Boolean(value);
16. String conversion
const s = String(42);
17. Narrowing event types
if (e instanceof MouseEvent) { e.clientX; }
18. Casting tuple
const p = [1, 2] as [number, number];
19. as const (not an assertion of type)
const COLORS = ['red', 'green'] as const;
20. Type predicate at API boundary
function isApiError(x: unknown): x is ApiError {
return typeof x === 'object' && x !== null && 'code' in x;
}
Visual: Assertion Is Compile-Time Only
┌──────────────────────────────────────────────┐
│ Source: │
│ │
│ const n = '42' as number; │
│ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Compiler sees: │
│ │
│ n has type number (asserted) │
│ n's value at runtime: '42' (unchanged) │
│ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Emitted JavaScript: │
│ │
│ const n = '42'; │
│ │
│ (assertion erased) │
│ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Runtime: │
│ │
│ typeof n === 'string' │
│ │
│ TypeScript said number — it's wrong │
│ │
└──────────────────────────────────────────────┘
Visual: Assertion vs Conversion
┌──────────────────────────────────────────────┐
│ Assertion (`as`) │
│ │
│ '42' as unknown as number │
│ → type: number │
│ → value: '42' (string) │
│ → crashes later when used as a number │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Conversion (`Number()`) │
│ │
│ Number('42') │
│ → type: number │
│ → value: 42 (number) │
│ → works as a number │
│ │
└──────────────────────────────────────────────┘
Visual: as vs satisfies
┌──────────────────────────────────────────────┐
│ `as Record<string, string>` │
│ │
│ const ROUTES = { home: '/' } as Record<...> │
│ │
│ → typeof ROUTES.home is string │
│ → literal '/' lost │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ `satisfies Record<string, string>` │
│ │
│ const ROUTES = { home: '/' } satisfies ... │
│ │
│ → typeof ROUTES.home is '/' │
│ → literal preserved │
│ → type checked against Record │
│ │
└──────────────────────────────────────────────┘
Visual: as vs Type Predicate
┌──────────────────────────────────────────────┐
│ `as` — no runtime check │
│ │
│ const u = data as User; │
│ u.name; // crashes if data isn't a User │
│ │
│ Compile time: trusted │
│ Runtime: unchecked │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ `is T` — runtime check │
│ │
│ function isUser(x: unknown): x is User { │
│ return typeof x === 'object' && ... │
│ } │
│ │
│ if (isUser(data)) { │
│ data.name; // safe — checked │
│ } │
│ │
│ Compile time: trusted │
│ Runtime: verified │
│ │
└──────────────────────────────────────────────┘
Visual: Non-Null Assertion
┌──────────────────────────────────────────────┐
│ Without `!` │
│ │
│ const el = document.getElementById('x'); │
│ // el: HTMLElement | null │
│ el.innerHTML = 'hi'; // ❌ possibly null │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With `!` — no check │
│ │
│ const el = document.getElementById('x')!; │
│ el.innerHTML = 'hi'; // ✅ compiles │
│ // ❌ crashes if element is missing │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With check — safe │
│ │
│ const el = document.getElementById('x'); │
│ if (el) { │
│ el.innerHTML = 'hi'; // ✅ narrowed │
│ } │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Tree
┌──────────────────────────────────────────────┐
│ Do you have a runtime check? │
│ │
│ ├── Yes ──► Use a type predicate `is T` │
│ │ │
│ └── No ──► Is it external data? │
│ │ │
│ ├── Yes ──► Add a check │
│ │ │
│ └── No ──► Is it a DOM element? │
│ │ │
│ ├── Yes ──► `as T` │
│ │ │
│ └── Other ──► `as T` │
│ (with │
│ comment) │
│ │
└──────────────────────────────────────────────┘
Visual: Assertion Safety Spectrum
┌──────────────────────────────────────────────┐
│ Safest │
│ │
│ satisfies T │
│ is T (type predicate) │
│ Narrowing (if/switch) │
│ `as T` between related types │
│ `!` after a check │
│ `as T` on DOM / libraries │
│ `as T` on untrusted data │
│ `as unknown as T` │
│ `as any` │
│ │
│ Riskiest │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
as T | Assert the value is T — compile-time only |
<T>x | Angle-bracket form — avoid in .tsx |
x! | Non-null assertion — removes null/undefined |
x as unknown as T | Double assertion — escape hatch |
satisfies T | Check the type — keep the inferred type |
is T predicate | Runtime check that narrows |
| Conversion | Number(), String(), Boolean() |
| Widening | as to a broader type — no-op |
| Narrowing | as to a narrower type — restricted |
Key takeaways:
- Type assertions are compile-time claims, not runtime conversions
as Tchanges the type — it doesn’t change the value- The angle-bracket form conflicts with JSX — use
as - Only related types can be asserted —
'hello' as numberfails,unknown as numberis fine as unknown as Tbypasses the restriction — treat it as a red flag!removesnull/undefined— use it only after a checksatisfies Tchecks the type without changing it — preserves literal types- Type predicates (
x is T) are runtime-checked and safer thanas - Assert at boundaries — DOM, JSON, libraries — not in the middle of typed code
- For numeric conversion, use
Number()/parseInt()— notas - Prefer
satisfiesoveraswhen checking configuration or constants - Prefer predicates over
asfor untrusted data
Remember: as doesn’t do anything at runtime. It’s a message to the compiler: “I know better than you.” That’s sometimes true — the compiler can’t know which HTML element getElementById returns, or what shape a JSON payload has. Use assertions where those facts live outside the type system, and validate external data with predicates. Prefer satisfies when you want a check without changing the type. Reach for ! only when you’ve already proven the value isn’t null. The compiler trusts you — that’s a privilege, not a shortcut.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!