First Principles
- Let your types be flexible
- Let your constraints be rigid
TL;DR Rules
- Use
unknown
instead ofany
, then use type narrowing to get the correct type. - Use
type
overinterface
, unless you actually need to reach for an interface or need to express objects/class inheritance. - Avoid using
as
to assert types, most of the time you actually want to narrow the type with checks (if/else). - Use
array.at(index)
instead ofarray[index]
unless array is a tuple (fixed size array). - NEVER use TS specifics (
enum
,private
in constructor, etc.).
Recommandations
- Use
satisfies
to check if an object fits a type but not erase the type. - Use
as const
whenever possible. (Immutable data, enum-like objects, etc.) - Define (and export) types where they are consumed, and import them from other files if needed.
Explanations
Narrowing over using as
Suppose you have a function that takes a number
function double(a: number) {return a * 2;}
And you have a variable that could be a number or a string
function getNumberOrString(): number | string {return Math.random() > 0.5 ? 1 : "1";}const a: number | string = getNumberOrString();
Typescript will allow you to use as
to assert the variable to a number (this is one of the ways that TypeScript is not sound)
const result: number = double(a as number);
But this not correct/sound at runtime!
The correct way to do this is to narrow the type with a check (if/else/early return).
if (typeof a === "number") {const result = double(a);}