Typorithm · hard

1. Assignability, unknown, any and never

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

1. Assignability, unknown, any and never

0%

Concept and contract

Reason about sets of values and the deliberate limits of structural compatibility.

Assignability asks whether a value usable through one type can satisfy another contract. Structural object compatibility focuses on available members rather than the declaration’s name. Thinking in sets helps: a literal belongs to a broader primitive type, and a union admits members of either alternative.

This model has intentional exceptions and unsound edges, especially any and mutable aliases. Treat it as a reasoning tool rather than a proof that TypeScript is a fully sound theorem prover. Runtime checks still establish external data and business invariants.

Mechanism and worked example

unknown is a safe receiving type for arbitrary values: operations require narrowing.

unknown is a safe receiving type for arbitrary values: operations require narrowing. never describes an impossible value, such as the remainder of an exhaustive union. any disables important checking and can flow through assignments in ways that destroy evidence.

The example accepts arbitrary input but performs only justified operations. A string is normalized; everything else is rejected. Returning never from fail records that it cannot return normally, which lets control flow continue with the narrowed branch after a failure.

Complete worked example

typescript

function fail(message: string): never { throw new Error(message); }
function normalize(value: unknown): string {
  if (typeof value !== "string") return fail("Text required");
  return value.trim();
}
Reasoning
The failure path cannot return; the remaining path has a checked string.

Counterexamples and common mistakes

Assigning unknown directly to string is rejected.

Assigning unknown directly to string is rejected. Replacing unknown with any would make the assignment compile without establishing any new fact. This is why an any leak from a JSON parser or dependency can weaken code far beyond the original boundary.

Intersection is not object merging at runtime. Conflicting property requirements can produce an impossible property type, while an assertion can still fabricate a statically accepted reference. Inspect the actual runtime construction instead of treating & as an executable combine operation.

Rejected counterexample: expected compiler diagnostic

typescript

const input: unknown = "QA";
// @ts-expect-error unknown requires evidence before assignment.
const title: string = input;
Reasoning
Receiving a value as unknown is safe, but using it as a string requires evidence.

Independent practice

Write requireCount accepting unknown and returning a finite integer.

Write requireCount accepting unknown and returning a finite integer. Throw for strings, NaN and fractions. Keep a separate fail helper returning never, then explain how the successful branch becomes number without an assertion.

Add positive zero and negative-integer cases unless your contract explicitly excludes them. This task checks the distinction between numeric shape and domain restrictions: integer does not automatically mean positive, finite or within a business-specific range.

Step 1: Narrow unknown with typeof. Step 2: Reject nonfinite or nonintegral numbers.

Practice starter: solve before reading the solution

typescript

function requireCount(value: unknown): number { throw new Error("TODO"); }
Reasoning
Define integer semantics explicitly rather than quietly assuming positivity.
  • Hint 1: Zero and negative integers remain valid here.
  • Hint 2: A never-returning failure helper cannot return normally.

Solution, tests and trade-offs

The solution narrows with typeof and validates finite integer behavior before returning.

The solution narrows with typeof and validates finite integer behavior before returning. Its fail helper throws, so the remaining path is valid. No generic or assertion is needed because the output contract is concrete and the runtime predicate establishes it.

Test rejected values and accepted boundaries independently. Then change fail to return a string and observe why the narrowing argument no longer supports a guaranteed number return. The compiler relationship should match the implementation’s actual control flow.

Practice solution and acceptance checks

typescript

function fail(message: string): never { throw new Error(message); }
function requireCount(value: unknown): number {
  if(typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) return fail("Finite integer required");
  return value;
}

// Acceptance checks
if(requireCount(0)!==0||requireCount(-2)!==-2)throw new Error("integers");for(const x of ["2",NaN,1.5]){let bad=false;try{requireCount(x)}catch{bad=true}if(!bad)throw new Error("invalid");}
Reasoning
The runtime predicate establishes the exact concrete return contract.

Interview reasoning and next step

For interviews, compare unknown and any using one unsafe property read and explain why never is not a universal substitute for missing data.

For interviews, compare unknown and any using one unsafe property read and explain why never is not a universal substitute for missing data. Describe an intersection conflict and a structural assignment with extra fields.

Next, preserve relationships through generic inference. Broad acceptance is useful only when the return type retains the information callers need; replacing every difficult relationship with unknown can be safe but unnecessarily unhelpful.

Optional stretch — after the core task works: Add a separate positive-count constructor.

Reason about sets of values and the deliberate limits of structural compatibility.