any, unknown, and never
If a type is a set of values, as what a type is established, then these three are the extremes. any and unknown are both the set of everything. never is the empty set.
The two wide ones differ in one respect, and it decides everything: with unknown, values go in and nothing comes out until you prove what you have. With any, values go in and come out again unchallenged. That makes unknown a boundary and any a hole, and this page spends its first half proving the difference.
Create programs/any-unknown-never.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:
import { assertEquals, assertThrows } from "@std/assert";
Follow the page as you add and revise the runnable examples below that import.
anything goes into unknown, and nothing comes out
unknown is the set of everything, so every assignment into it is a membership success. Assignment out of it is another story. Try both directions and save:
Deno.test("anything goes into unknown, and nothing comes out", () => {
let anything: unknown;
anything = null;
anything = true;
anything = { total: 10 };
assertEquals(anything, { total: 10 });
const value: unknown = "oak";
const text: string = value;
assertEquals(text, "oak");
});
Check programs/any-unknown-never.test.ts
TS2322 [ERROR]: Type 'unknown' is not assignable to type 'string'.
const text: string = value;
~~~~
at file:///programs/any-unknown-never.test.ts:12:11
error: Type checking failed.
info: The program failed type-checking, but it still might work correctly.
hint: Re-run with --no-check to skip type-checking.
Both directions, one rule.
null,true, and an object all flow intoanythingwithout complaint, because every value is a member of the set of everything.- Flowing out fails, and the reason is the same subset logic running in reverse. The target type of
textaccepts members ofstring, and anunknowncould be holding anything at all, so the checker cannot prove membership and refuses.
The value in question really is a string, so the refusal can be pinned as the claim it is:
Deno.test("anything goes into unknown, and nothing comes out", () => {
let anything: unknown;
anything = null;
anything = true;
anything = { total: 10 };
assertEquals(anything, { total: 10 });
const value: unknown = "oak";
// @ts-expect-error: unknown satisfies no other type until narrowed
const text: string = value;
assertEquals(text, "oak");
});
Check programs/any-unknown-never.test.ts
running 1 test from ./programs/any-unknown-never.test.ts
anything goes into unknown, and nothing comes out ... ok (0ms)
ok | 1 passed | 0 failed (1ms)
any is the same set with the checking turned off
any also accepts everything. The difference is what happens on the way out:
Deno.test("any is the same set with the checking turned off", () => {
// deno-lint-ignore no-explicit-any
const loose: any = "oak";
const count: number = loose;
assertEquals(typeof count, "string");
});
any is the same set with the checking turned off ... ok (0ms)
Read what just happened, because the quietness is the event.
looseholds a string, and it flows intocount, whose target type isnumber, with no diagnostic produced anywhere.- The runtime assertion confirms the damage:
typeof countis"string". A string is sitting in a variable annotatednumber, and the file checks clean. - That is not a bug in TypeScript.
anymeans "stop checking", and it did. Anunknownsatisfies no other type; ananysatisfies every type, so every target type accepts it.
One more voice in the room: the deno-lint-ignore line exists because Deno's linter refuses an explicit any by default, through the no-explicit-any rule. To write one at all, you sign for it on the line above, which is exactly the right amount of friction for a hole.
the hole in one line: JSON.parse returns any
JSON.parse returns any, because what it returns genuinely depends on the text you gave it. Feed it a string while the annotation says number, predict what typeof reports, and save:
Deno.test("the hole in one line: JSON.parse returns any", () => {
const total: number = JSON.parse('"not a number"');
assertEquals(typeof total, "number");
assertThrows(() => total.toFixed(2), TypeError);
});
Check programs/any-unknown-never.test.ts
running 3 tests from ./programs/any-unknown-never.test.ts
anything goes into unknown, and nothing comes out ... ok (1ms)
any is the same set with the checking turned off ... ok (0ms)
the hole in one line: JSON.parse returns any ... FAILED (8ms)
ERRORS
the hole in one line: JSON.parse returns any => ./programs/any-unknown-never.test.ts:24:11
error: AssertionError: Values are not equal.
[Diff] Actual / Expected
- string
+ number
FAILURES
the hole in one line: JSON.parse returns any => ./programs/any-unknown-never.test.ts:24:11
FAILED | 2 passed | 1 failed (10ms)
error: Test failed
total is a string. The annotation promised number, JSON.parse's any waved the string through, and the checker saw nothing wrong with any of it. Correct the prediction to "string" and keep the assertThrows, which shows where this ends in a real program: something eventually calls a number method on the value, and the program dies far from the line that lied.
the hole in one line: JSON.parse returns any ... ok (0ms)
Every argument for unknown is this example. JSON.parse predates unknown, which is the only reason its signature still says any. Treat its result as unknown yourself: assign it to an unknown and narrow, the way a predicate teaches the checker a check of your own did on the unions and narrowing page, or hand it to a validator that returns a real type.
Deno will not give you an implicit any
Historically, any also arrived uninvited: a parameter without a type silently became one. Under Deno's defaults that is an error instead:
Deno.test("Deno will not give you an implicit any", () => {
// @ts-expect-error: a parameter without a type is an error, not an any
function greet(name): string {
return `Hello ${name}`;
}
assertEquals(greet("world"), "Hello world");
});
Deno will not give you an implicit any ... ok (0ms)
The pinned diagnostic is TS7006: Parameter 'name' implicitly has an 'any' type. The consequence is worth stating in full: the any values in your program are the ones you wrote on purpose, each carrying its lint-ignore signature, plus the ones arriving from library signatures like JSON.parse. Auditing them is a finite job, which is a much better position than it sounds.
the honest way out of unknown is a question
An unknown is unusable on purpose, and there are two ways out. The honest one is to ask a question the checker understands:
Deno.test("the honest way out of unknown is a question", () => {
const value: unknown = "oak";
// @ts-expect-error: unknown is unusable until narrowed
const eager = value.length;
assertEquals(eager, 3);
if (typeof value === "string") {
assertEquals(value.length, 3);
}
});
the honest way out of unknown is a question ... ok (0ms)
Two reads of the same property, two verdicts.
- The eager read is pinned:
TS18046: 'value' is of type 'unknown'.The runtime line underneath shows the read works this time, because the value happens to be a string, which is precisely the kind of luck the checker refuses to rely on. - Inside the
typeofguard,valuehas narrowed fromunknowntostring, and the same read is legal. Everything the unions-and-narrowing page's the checks that narrow are ordinary JavaScript established applies here:typeof,instanceof,Array.isArray, literal comparison, a predicate, an assertion function. What you learn this way is checked.
as is a claim, not a conversion
The second way out is to assert, with as, that you know better. The checker resists when the two types share no members. Try it directly:
Deno.test("as is a claim, not a conversion", () => {
const direct = "oak" as number;
assertEquals(typeof direct, "string");
});
Check programs/any-unknown-never.test.ts
TS2352 [ERROR]: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
const direct = "oak" as number;
~~~~~~~~~~~~~~~
at file:///programs/any-unknown-never.test.ts:50:20
error: Type checking failed.
info: The program failed type-checking, but it still might work correctly.
hint: Re-run with --no-check to skip type-checking.
Read the whole message, because its last sentence is remarkable: If this was intentional, convert the expression to unknown first. The checker tells you how to defeat it. Take the suggestion and watch what it buys:
Deno.test("as is a claim, not a conversion", () => {
const value: unknown = "oak";
const doubled = value as number;
assertThrows(() => doubled.toFixed(2), TypeError);
});
as is a claim, not a conversion ... ok (0ms)
value as number compiles, because unknown converts to anything, and the assertThrows records what the claim was worth: the first number method call crashes. That is the honest summary of as. It is not a conversion and it performs no check; it is a claim taken on trust, and a wrong one buys a crash later instead of an error now. Every as unknown as X these references have written, starting back in the values-and-references page, is this maneuver twice over, and it should always feel expensive.
never vanishes from a union, and unknown absorbs one
never is the empty set, and its algebra follows from that. Type-level claims need type-level evidence, and assignability in both directions is the tool: two sets that each fit inside the other are the same set.
Deno.test("never vanishes from a union, and unknown absorbs one", () => {
type WithNever = "a" | "b" | never;
const forward: WithNever = "a";
const backward: "a" | "b" = forward;
assertEquals(backward, "a");
const anything: unknown = 42;
const absorbed: unknown | string = anything;
assertEquals(absorbed, 42);
});
never vanishes from a union, and unknown absorbs one ... ok (0ms)
Two identities, proved by flow.
forwardcarries aWithNeverinto a variable of plain type"a" | "b", and the checker accepts, soWithNeverfits inside"a" | "b". The other direction is trivial. Adding the empty set to a union contributed no members, sonevervanished from it.absorbedaccepts a bareunknownholding42, a number, not a string, sounknown | stringaccepts everythingunknowndoes. Adding everything to a set absorbs it: the union is justunknown.
the checker answers never when no value could qualify
never also appears where you never wrote it, as the checker's way of saying "no value could satisfy this". Two constructions force its hand:
Deno.test("the checker answers never when no value could qualify", () => {
type NoKeys = keyof {};
// @ts-expect-error: an empty object has no keys, so keyof gives never
const key: NoKeys = "a";
assertEquals(String(key), "a");
type Impossible = boolean & symbol;
// @ts-expect-error: no value is both a boolean and a symbol
const collapsed: Impossible = true;
assertEquals(collapsed, true);
});
the checker answers never when no value could qualify ... ok (0ms)
Neither declaration mentions never, and both pinned diagnostics do.
- Assigning to
NoKeysdrawsType '"a"' is not assignable to type 'never'.keyof {}asks for the set of keys of an object with no properties, and the set of no keys is the empty set. - Assigning to
ImpossibledrawsType 'true' is not assignable to type 'never'. Intersectingbooleanandsymbolasks for values belonging to both sets, and no value does, so the intersection collapses to empty. Whenneverturns up in an error message you did not expect, this is usually what happened: somewhere, an intersection or a lookup produced a set with nothing in it.
nothing is assignable to never
The defining property of the empty set is that membership always fails, which makes never useful as a prohibition:
Deno.test("nothing is assignable to never", () => {
// @ts-expect-error: never is the empty set
const nothing: never = 1;
assertEquals(nothing, 1);
// @ts-expect-error: each value would have to come from the empty set
const forbidden: Record<string, never> = { total: 123 };
assertEquals(forbidden.total, 123);
const allowed: Record<string, never> = {};
assertEquals(Object.keys(allowed).length, 0);
});
nothing is assignable to never ... ok (0ms)
Three flows, one property.
1cannot enter the empty set, and nothing else can either.Record<string, never>describes an object whose string keys each hold a value from the empty set, which is a precise way of saying "no properties". The object withtotalis refused; the empty object is the only member.- This same property powers the exhaustiveness trick from the unions-and-narrowing page's
neverturns a forgotten case into a compile error: assigning a leftover case toneversucceeds only when the checker has proved nothing is left.
a function typed never cannot return
The last home of never is a return type, where it promises the function will not finish normally: it throws, or it loops forever. The checker spends that promise on the code after the call:
Deno.test("a function typed never cannot return", () => {
function fail(message: string): never {
throw new Error(message);
}
function required(value: string | undefined): string {
if (value === undefined) fail("missing");
return value;
}
assertEquals(required("oak"), "oak");
assertThrows(() => required(undefined), Error, "missing");
});
a function typed never cannot return ... ok (1ms)
Look at required closely, because something is missing.
- There is no
elseand no earlyreturn, and yetreturn valuetype-checks withvaluenarrowed tostring. - The narrowing works because
failreturnsnever: control cannot come back from a function whose return set is empty, so any line after the call is reachable only when the call did not happen, and on that pathvaluewas notundefined. - The two assertions exercise both paths: the string comes through, and the
undefinedends in the promised throw.
Annotate your throw helpers never and they do this work at every call site. Annotate them void and every caller has to convince the checker separately.
In practice
- Use
unknownwherever unchecked data enters your program. Narrow it once, in one place, and pass a real type onward. - Treat
JSON.parseas returningunknown, even though its signature says otherwise. - Keep the implicit-any error and
no-explicit-anylint rule enabled so every use ofanyis deliberate and countable. - Prefer narrowing to asserting. When an assertion is necessary, keep the
asbeside the reason it is safe. - Annotate throw helpers as returning
never. An unexpectednevererror means the checker believes no value can reach that point.