There is a phase every developer new to a type system goes through: annotate absolutely everything, just in case, the way a rookie houseguest overpacks for a season that turns out to have a laundry room. You have been at this three weeks now. Time to unpack a little. TypeScript has been quietly capable of figuring out plenty of these types on its own the whole time, you just have not been letting it.
Annotation hoarding, and the JSDoc habit that causes it
If you have ever written plain JS and wanted type safety anyway, you have probably reached for
JSDoc comments, @param, @returns, @type, bolted on above a function so your editor could at
least guess. That habit carries over into TypeScript as a reflex: annotate the parameter, the
return, the variable, every single one, because that is what "typed" used to mean. In real
TypeScript, half of that is the compiler's job now.
The ES6 you write
/**
* @param {string[]} nominees
* @returns {number}
*/
function nomineeCount(nominees) {
return nominees.length
}
/** @type {number} */
const week = 4The TS version
function nomineeCount(nominees: string[]) {
return nominees.length
}
const week = 4What changed
The JSDoc comments disappear entirely, they were doing the compiler's job by hand. nominees still needs its own annotation, TypeScript cannot know a bare parameter's type from nothing. But the return type and week's type are both gone on the right and nothing got less safe: TypeScript reads the function body and infers number for the return, reads the literal 4 and infers a number for week, same guarantees, less typing.
That is inference: TypeScript looking at what a value or expression actually is and assigning it a
type without being told. It happens constantly, whether you notice or not. Hover nomineeCount in
your editor right now, in the version with no return annotation, and it still says the return type
is number. Hover week and it says number too. Array literals get the same treatment: write
const evicted = ['Matt', 'Lisa'] with no annotation at all, and TypeScript infers string[]
on its own, by looking at what is actually inside the brackets.
as const: stopping literal widening
There is one place inference works against you, and it is worth seeing on purpose. A const
holding a single primitive keeps its exact literal type, const week = 4 really is the type 4,
not just number, for as long as you never reassign it. But put values inside an array or object,
and TypeScript widens each one back to the general type, on purpose, because it assumes you might
push a different string into that array later.
const roundTypes = ['veto', 'nomination', 'eviction']
// inferred as string[], every element widened to the general typeas const tells TypeScript to stop widening and trust the literal values exactly as written,
locking the array down to a readonly tuple of its exact contents.
const ROUND_TYPES = ['veto', 'nomination', 'eviction'] as const
// inferred as readonly ['veto', 'nomination', 'eviction']typeof, at the type level
You already know typeof as a runtime operator: typeof someValue === 'string' checks a value
while your code is running and hands back a string like "string" or "number". TypeScript
overloads the same keyword for a second job that only exists at compile time: used inside a type
position, typeof someValue means "give me the type of this value," not a string describing it.
Combine it with as const and you can derive a union straight from data you already wrote once,
instead of retyping the same three strings as a type.
type RoundType = (typeof ROUND_TYPES)[number]
// 'veto' | 'nomination' | 'eviction', derived, never retyped by hand(typeof ROUND_TYPES) is the tuple's type. [number] reads "give me the type of every element
this tuple could return when indexed by a number," which is the union of all three literals. One
array, written once, is now both the runtime data and the type.
That is the whole lesson. Now prove it in the comp below.