Source: Understanding typescript
Core value types & type inference
TypeScript starts with the same everyday primitives you already know from JavaScript:
boolean,string,number
From there, TypeScript’s superpower is type inference: if the compiler can confidently infer a type from how you initialize something, you usually don’t need to annotate it. This keeps code terse while staying type-safe. (typescriptlang.org )
const vs let inference (and “widening”)
consttends to infer literal types for primitives (e.g.,"hello"instead ofstring,42instead ofnumber) because the value can’t change.lettends to infer a wider type (e.g.,string) because the value might change later.
A subtle but important nuance: objects and arrays declared with const can still be mutated (their reference is constant, not their contents). That’s why object properties often still widen to string, number, etc. If you want “freeze-like” literal inference through an object/array, you’ll typically use const assertions (as const).
Object types, shapes, and “excess property” checks
“Objects remember property lists”
TypeScript is (mostly) structural: an object’s type is defined by the shape (its property names + value types). That’s why “objects remember property lists” is a useful mental model.
Object type syntax
In object types, properties are separated by ; or , (both work). Example:
type User = {
id: string;
role: "admin" | "user";
}object is a built-in type
object(lowercase) means “any non-primitive value” (notstring,number,boolean,symbol,bigint,null,undefined).Object(uppercase) is the JS wrapper type and is usually not what you want for typing.
“TS throws an error if you only partially type an object”
Two related behaviors often show up here:
- Missing required properties → error.
- Excess property checks: when you assign an object literal to a target type, TS checks it more strictly and rejects unknown fields (common with “options objects”). (typescriptlang.org )
“You can specify value in object type”
Yes: with literal types, e.g. role: "admin" or status: 200. This is the foundation for discriminated unions later.
Arrays, unions, for...of, and tuples
Union-typed arrays
If an array can contain more than one type, TS infers a union element type:
(string | number)[]
That’s often exactly what you want when data is genuinely mixed.
for...of
TypeScript fully supports for...of. Where it gets interesting is compilation: if you target older JS versions and iterate over non-arrays (like Map, Set, strings, etc.), you may need downlevelIteration to preserve correct iteration semantics.
Tuples
Tuples are fixed-length arrays with position-specific types, e.g.:
[number, string]
Two key gotchas:
- Explicit typing of tuples is legal (and recommended when the positions matter).
pushis allowed on tuples because tuples are implemented as arrays at runtime; the type system can’t perfectly prevent all array mutations. This is one reason many teams treat tuples as “don’t mutate” values by convention.
Enums (and the “union of constants” idea)
Enums are supported by TypeScript, but it’s worth knowing what you’re buying:
- Enums create a runtime object in emitted JS (unlike most TS-only types).
- Many modern TS codebases prefer unions of string literals (
"pending" | "done") for “enum-like” modeling because they are type-only and tree-shake cleanly.
Your note “union of constants” is conceptually pointing at that idea: literal unions often replace enums for domain states.
any, unknown, and never
any
any disables type safety for that value. Use it only when you truly must (e.g., migrating legacy code, or temporarily handling untyped third-party data).
unknown
unknown is the safer alternative to any:
- Anything can be assigned to
unknown. - But
unknowncan’t be assigned from (tostring,number, etc.) until you narrow it with checks.
This fits real-world input: user input, JSON payloads, external APIs, etc.
never
never represents a value that cannot happen.
Common cases:
- A function that throws (no value is produced)
- An infinite loop
- Exhaustiveness checks in
switchstatements
Functions and function types
void return type
void means “we don’t use a return value here.” At runtime, a function with no return returns undefined anyway.
Also: a function typed as returning void can still technically return something — TS is saying callers should ignore it. (typescriptlang.org )
Avoid the Function type for real typing
While Function exists, it’s usually too vague. Prefer function signatures:
(...args) => returnType
These signatures are what make callbacks type-safe and ergonomic.
Compiler workflow: tsc watch mode
For a single file:
tsc fileName -w
For a project:
- Run
tsc --initonce (createstsconfig.json) - Run
tsc -wto watch and compile the whole project
Also note: if you pass input files on the command line (e.g. tsc index.ts), TypeScript uses compiler defaults and ignores tsconfig.json. (typescriptlang.org )
tsconfig.json essentials (what the flags really control)
include / exclude
includedeclares what file patterns belong to the program.excluderemoves files from whatincludewould find—but it does not prevent a file from entering the program viaimport,types, references, etc. (typescriptlang.org )- Default
includeis**/*(unlessfilesis specified). (typescriptlang.org ) - Default
excludeincludesnode_modules,bower_components,jspm_packages, andoutDir. (typescriptlang.org )
About your glob examples:
*.dev.tsmatches only in the current folder.**/*.dev.tsmatches recursively. Use forward slashes (/) in globs for portability across OSes.
target
target controls which JS features TS downlevels.
One important “this changed over time” detail: TypeScript’s default target now depends on your module setting (e.g. NodeNext vs others). In the TSConfig reference:
- default is
es2023ifmoduleisnode20 - default is
esnextifmoduleisnodenext - default is
ES5otherwise (typescriptlang.org )
lib
lib decides which built-in API type definitions are available (DOM, ES features, etc.). If you set lib explicitly, you override the defaults that would otherwise be chosen based on target. (typescriptlang.org )
allowJs and checkJs
allowJslets JS files participate in the program (useful for gradual migration). (typescriptlang.org )checkJs(paired withallowJs) enables type-checking for JS files — similar to adding// @ts-checkeverywhere. (typescriptlang.org )
jsx
Controls TSX handling (React setups typically use react-jsx or preserve, depending on the toolchain).
.d.ts (declaration files)
Declarations are the “types-only surface area” of libraries. Consumers get type info without seeing your source. It’s a big topic because it affects packaging, module resolution, and what you promise to downstream users.
sourceMap
Enables .js.map output so debuggers can map emitted JS back to your original TS. (typescriptlang.org )
rootDir and outDir
These define input and output folder structure (commonly src → dist).
Emit controls
removeComments: strips comments from output.noEmit: type-check only; don’t write JS.noEmitOnError: if any error exists, emit nothing (even for “unrelated” files).
downlevelIteration
Helps make iteration constructs behave correctly when targeting older JS runtimes (often at the cost of more verbose output).
Strictness flags
strictenables a bundle of strictness settings.noImplicitAnyprevents “silent any” (explicitanyis still allowed).strictNullChecksforces you to handlenull/undefinedexplicitly instead of letting them slip into everything. (typescriptlang.org )strictBindCallApplytightens typing forbind,call, andapply.
Practical note from your comment: avoid the non-null assertion operator (!) if something can genuinely be missing — it’s a way to silence the compiler, not a runtime guarantee.
Cleanliness flags
noUnusedLocals,noUnusedParametersnoImplicitReturns
These are “keep the codebase honest” flags: they reduce dead code and ambiguous control flow.
Modern JavaScript features you’ll use constantly in TS
letandconst- Arrow functions
- Default parameters (best placed at the end of the parameter list)
- Spread (
...arr) and rest parameters ((...args)) - Destructuring (doesn’t mutate the original object/array)
Classes: constructors, this, access modifiers, inheritance
Basic class + constructor
class Department {
name: string;
constructor(n:string){
this.name = n;
}
}This compiles cleanly to modern JS classes when targeting ES6+.
How compilation differs by target
ES6 output:
class Department{
constructor(n){
this.name = n;
}
}ES5 output (IIFE + function constructor pattern):
var Department = (function (){
function Department(n) {
this.name = n;
}
return Department
}());
var accounting = new Department('Accounting');
console.log(accounting);Methods, and why this: Department is useful
class Department {
name: string;
constructor(n:string){
this.name = n;
describe(this: Department) { // doesn't have to be initialized, but binds the context for the function.
console.log('Department: '+this.name);
}
}
}
const accounting = new Department('Accounting')
acounting.describe();
const accountingCopy = { name: 's'; describe: accounting.describe };The key idea: adding a this parameter type is a TypeScript-only way to force correct call context. If someone does const f = accounting.describe; f(), TS can warn you because this is missing/badly bound.
(Also: the snippet has a few typos/structure issues — in real TS you wouldn’t nest describe inside the constructor block, and accounting is misspelled — but the concept you’re capturing is correct: type the this context to prevent unbound calls.)
private, parameter properties, and readonly
class Department{
constructor(private id: string, private name: string){
// no need for assignments if access modifiers are placed as above.
}
}That pattern is called parameter properties: putting private/public/protected/readonly on constructor parameters auto-creates and initializes fields.
With readonly:
class Department{
constructor(private readonly id: string, private name: string){
// no need for assignments if access modifiers are placed as above.
}
}Inheritance, super, and protected
You captured the standard flow:
extendscreates a subclasssuper(...)must be called before accessingthisin a derived constructorprotectedallows subclasses to access members while keeping them hidden from outside code
(Your later snippets demonstrate this progression.)
Getters and setters
get mostRecentReport(){
if(this.lastReport){
return this.lastReport;
}
throw new Error('No report found.')
}
set mostRecentReport(value: string) {
if(!value) return;// or throw error.
this.addReport(value);
}Use getters/setters when you want property-like access with validation or side effects, while still offering an ergonomic API.
Static members
static fiscalYear = 2021;
static createEmployee(name: string){//static method
return {name: name};
}Static members belong to the class, not instances, and are accessed like Department.createEmployee(...).
Abstract classes
abstract class Department{// because it has one abstract method.
...
abstract describe(this: Department): void;
...
}Abstract classes:
- cannot be instantiated
- can force subclasses to implement required methods (like
describe)
Private constructors + Singleton pattern
private constructor(id:string, private reports: string[]){// cannot call new on this, since constructor is private.
super(id,'Accounting');
this.lastReport = reports[0];
}
static getInstance(){
if(AccountingDepartment.instance){
return this.instance;
}
return new AccountingDepartment('d2',[]);
}// singleton ensured.This enforces “only one instance exists,” while still allowing instance methods (instead of forcing everything static).
Interfaces: modeling contracts (and how they differ from types)
Basic interface
interface Person {
name: string;
age: number;
greet(phrase:string):void;
}
let user1: Person; // requires that user1 be of person type.
user1 = {
name: 'Max',
age: 27
greet(phrase:string){
console.log(phrase);
}
};
user1.greet('Hi, there');Interfaces define structure only (no default values, no runtime output).
Interfaces vs type aliases
Your note is directionally right:
- Interfaces are best for object shapes and contracts (especially with
implementsand declaration merging). - Type aliases can name anything: primitives, unions, tuples, function types, etc. (typescriptlang.org )
Small correction to “Interfaces cannot store union types”: An interface can have union-typed properties (
id: string | number), but an interface itself can’t be a union in the way a type alias can (type X = A | B).
implements (interfaces have no implementation)
interface Greetable {
name: string;
greet(phrase:string):void;
}
class Person implements Greetable/*, AnotherInterface*/ {
name: string
constructor(n:string){
this.name = n;
}
greet(phrase:string){
console.log(phrase+` ${this.name}`);
}
}
user1 = new Person('Max')readonly, extends, optional props/methods
interface Greetable {
readonly name: string;
greet(phrase:string):void;
}interface Named {
readonly name: string;
}
interface Greetable extends Named{
greet(phrase:string):void;
}interface Named{
readonly name: string;
outputName?: string;
myMethod?(n?:string): void // optional method with optional arguement returning nothing.
}Function “shape” via interface
// type AddFn = (a:number, b:number)=> number;
interface AddFn {
(a: number, b: number):number;
}Interfaces don’t compile to JS
There’s no runtime output for interfaces — they exist purely at compile time.
Advanced concepts to learn next
You’ve flagged the right set of “Level 2” TS topics:
- Intersection types
- Type guards / narrowing
- Discriminated unions
- Type casting (careful: it’s not runtime conversion)
- Function overloads
Those are where TS starts feeling expressive rather than just “typed JavaScript.”