Skip to Content
BlogCodeUnderstanding TypeScriptunderstanding typescript 1

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”)

  • const tends to infer literal types for primitives (e.g., "hello" instead of string, 42 instead of number) because the value can’t change.
  • let tends 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” (not string, 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:

  1. Missing required properties → error.
  2. 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).
  • push is 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 unknown can’t be assigned from (to string, 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 switch statements

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:

  1. Run tsc --init once (creates tsconfig.json)
  2. Run tsc -w to 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

  • include declares what file patterns belong to the program.
  • exclude removes files from what include would find—but it does not prevent a file from entering the program via import, types, references, etc. (typescriptlang.org)
  • Default include is **/* (unless files is specified). (typescriptlang.org)
  • Default exclude includes node_modules, bower_components, jspm_packages, and outDir. (typescriptlang.org)

About your glob examples:

  • *.dev.ts matches only in the current folder.
  • **/*.dev.ts matches 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 es2023 if module is node20
  • default is esnext if module is nodenext
  • default is ES5 otherwise (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

  • allowJs lets JS files participate in the program (useful for gradual migration). (typescriptlang.org)
  • checkJs (paired with allowJs) enables type-checking for JS files — similar to adding // @ts-check everywhere. (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 srcdist).

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

  • strict enables a bundle of strictness settings.
  • noImplicitAny prevents “silent any” (explicit any is still allowed).
  • strictNullChecks forces you to handle null/undefined explicitly instead of letting them slip into everything. (typescriptlang.org)
  • strictBindCallApply tightens typing for bind, call, and apply.

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, noUnusedParameters
  • noImplicitReturns

These are “keep the codebase honest” flags: they reduce dead code and ambiguous control flow.

Modern JavaScript features you’ll use constantly in TS

  • let and const
  • 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:

  • extends creates a subclass
  • super(...) must be called before accessing this in a derived constructor
  • protected allows 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 implements and 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.”