unknown, any, and never
The three types people misuse most: what each one means, why any disables the compiler far beyond the line it appears on, and what never is telling you when it shows up.
The three types people misuse most: what each one means, why any disables the compiler far beyond the line it appears on, and what never is telling you when it shows up.
A helper was typed (...args: any[]) => any two years ago
because the signature was awkward. Nothing in the file that uses
it has been type-checked since — not the arguments, not the
result, not anything computed from the result. The checker has
been running the whole time and reporting nothing, and everybody
believes the module is covered.
Three types get misused more than everything else combined. By the end of this lesson you will know exactly what each means, why one of them spreads, and which to reach for when you genuinely do not know what a value is.
let value: any = 42;
value.toUpperCase(); // no error
value.anything.at.all(); // no error
value(); // no error
const n: number = value; // no errorany means "stop checking this". Every operation is allowed and
every assignment is allowed, in both directions.
That last part is what makes it dangerous. An any can be
assigned to any type without complaint, so it does not stay
where you put it:
function parseConfig(text: string): any {
return JSON.parse(text);
}
const config = parseConfig(raw);
const timeout: number = config.timeuot; // no error
setTimeout(handler, timeout); // NaN at runtimeThe typo compiles. timeout is declared number and holds
undefined, because the any was assignable to it. Everything
downstream is now working with a number that is not one, and the
checker approved all of it.
any is contagious. One vague return type disables checking
across a whole call chain, and the further the value travels the
harder it is to see that anything was ever unchecked.
let value: unknown = 42;
value.toUpperCase(); // error
const n: number = value; // error
if (typeof value === "number") {
const n: number = value; // fine - proven
}unknown is the honest version. Anything can be assigned to
it, and it can be assigned to nothing until you narrow it.
I do not know, so allow everything.
Assignable to any type, so it escapes the place you wrote it and takes the checker off wherever it lands.
The mistake surfaces at runtime, far from the annotation that permitted it.
I do not know, so allow nothing until someone checks.
Anything can be put into it and it can be assigned to nothing until narrowed.
The obligation to check lands on the person holding the value, which is exactly where it belongs.
Now the caller must validate, which is the correct obligation — this data came from a file and nothing has verified it. The type predicates from the previous lesson are how you discharge it.
Whenever you would write any, try unknown first. It is
usually the correct type and it contains the damage rather than
spreading it.
Bad — any to make an awkward
signature compile.
Good — generics that preserve the signature.
The first version does not just lose type information about
itself. Every function passed through it comes back accepting
anything and returning any, so a wrapper applied to twenty
functions removes checking from twenty call sites — and it looks
like infrastructure rather than a hole.
This is the most common way type coverage is lost in a real codebase, and it is the opening scenario of this lesson.
The genuinely legitimate uses of any are narrow:
Even there, unknown usually works and forces the boundary to
be explicit.
never means "no value can be here". It appears in three
situations, and it is a diagnosis rather than something you
usually write.
A function that does not return:
Annotating it lets the checker know execution stops, so code
after a fail() call is correctly seen as unreachable.
Exhaustiveness, from the previous lesson:
An impossible type, which is where it becomes a diagnostic:
That is the useful one. When a type resolves to never
unexpectedly, an intersection somewhere has no possible values:
A variable that mysteriously "has no properties" is usually
never. Hover over it, and then look for the intersection.
The related empty-ish types are worth distinguishing:
{} surprises people: it accepts 42 and "text", because a
number does have all zero of the required properties. It is
almost never what someone means — use object for
non-primitives, or Record<string, unknown> for a dictionary.
Most any in a codebase was never typed by anyone. It comes
from untyped dependencies, implicit parameters and index
signatures.
noImplicitAny is part of strict and catches parameters. It
does not catch a value that became any by flowing out of an
untyped library — for that, a lint rule:
The four no-unsafe-* rules are the valuable ones, and they are
the only way to see the second-hand any flowing through your
code. no-explicit-any catches the ones people wrote
deliberately, which are usually the smaller problem.
Turn them on one at a time, as the strict lesson advised. The first run on an untouched codebase is informative regardless of whether you fix it that week.
You now know that any is not a weaker type but an off switch,
that unknown is what you almost always meant, and that never
is usually a diagnosis rather than a declaration. The four
no-unsafe-* lint rules are the action item — they are the only
way to see the any you inherited rather than wrote.
Next is Modelling Domains with Types, which puts the whole type system to work on a single goal: making states your program cannot handle impossible to construct. The unions and generics from the last three lessons are the tools; this is what they are for.
Before you move on, turn on no-unsafe-member-access in one
directory and read what it finds. Every hit is a place where the
checker has been silently doing nothing, and in most codebases
there are more of them than anyone expects.
any stop checking
every operation allowed; assignable TO any type
so it does not stay put - it spreads down the call chain
a typo on an `any` compiles and produces undefined in a number
unknown I do not know, so nothing is allowed yet
anything is assignable to it; it is assignable to nothing
narrow with typeof / instanceof / a type predicate
whenever you would write any, try unknown first
never no value can be here
a function that always throws: fail(msg): never
the exhaustive default: assertNever(value)
an impossible type: string & number
a variable with "no properties" is usually never
string | never is string - which is how conditional types filter
RELATED
void a return nobody should use
undefined one specific value
{} anything except null/undefined - accepts 42 and "text"
object any non-primitive <- usually what {} meant
Record<string, unknown> <- for a dictionary
THE WRAPPER THAT LOSES EVERYTHING
(...args: any[]) => any
every function passed through comes back unchecked
-> <A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R
LEGITIMATE any
inside a constraint meaning "any function"
one boundary with an untyped library - and unknown usually works
FINDING IT
noImplicitAny catches parameters, not inherited any
@typescript-eslint/no-unsafe-assignment / -member-access
/ -call / -return
those four find the SECOND-HAND any, which is most of it
no-explicit-any catches only the deliberate onesfunction parseConfig(text: string): unknown {
return JSON.parse(text);
}
const config = parseConfig(raw);
config.timeout;
// ~~~~~~~ 'config' is of type 'unknown'function withLogging(fn: (...args: any[]) => any, name: string) {
return (...args: any[]): any => {
console.log(`calling ${name}`);
return fn(...args);
};
}
const load = withLogging(loadPhoto, "loadPhoto");
load(42, "wrong", true); // no error
load("abc").nonexistent; // no errorfunction withLogging<A extends unknown[], R>(
fn: (...args: A) => R,
name: string,
): (...args: A) => R {
return (...args: A): R => {
console.log(`calling ${name}`);
return fn(...args);
};
}
const load = withLogging(loadPhoto, "loadPhoto");
load(42); // error - wrong argument type// inside a generic constraint, where it means "any function"
type AnyFunction = (...args: any[]) => any;
// interoperating with an untyped library, at ONE boundary
const legacy = untypedModule as any;function fail(message: string): never {
throw new Error(message);
}default:
return assertNever(upload); // upload is never heretype Impossible = string & number; // nevertype A = { kind: "photo"; size: number };
type B = { kind: "album"; count: number };
type Both = A & B;
type K = Both["kind"]; // never - no value is both literalsnever no value at all
void a return value nobody should use
undefined one specific value
{} anything except null and undefined
object any non-primitive{
"compilerOptions": {
"noImplicitAny": true,
"noImplicitReturns": true
}
}{
"rules": {
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-return": "error",
"@typescript-eslint/no-explicit-any": "warn"
}
}