Output & Running
Hello, World
The smallest complete program in each language — the TypeScript column is the whole file.
Option Strict On
Imports System
Module HelloWorld
Sub Main()
Console.WriteLine("Hello, World!")
End Sub
End Moduleconsole.log("Hello, World!");There is no module, no entry point and no imports. A TypeScript file runs its statements top to bottom, like a script.
console.log is the counterpart of Console.WriteLine, and it is available everywhere without importing anything. The name is a reminder of where the language came from: this is the browser's developer console, and the same function is what Node.js prints with.String interpolation
Interpolation exists in both, with backticks in place of the
$ prefix and a dollar sign moving inside.Option Strict On
Imports System
Module InterpolationDemo
Sub Main()
Dim name As String = "Ada"
Dim score As Integer = 42
Console.WriteLine($"Hello, {name}! Score: {score}")
Console.WriteLine($"Doubled: {score * 2}")
End Sub
End Moduleconst name = "Ada";
const score = 42;
console.log(`Hello, ${name}! Score: ${score}`);
console.log(`Doubled: ${score * 2}`);A template literal is delimited by backticks rather than quotes, and each hole is
${...} rather than {...}. Any expression may go inside. There are no format specifiers: {score:D5} has no template-literal equivalent, and formatting is done with methods instead — score.toString().padStart(5, "0"), ratio.toFixed(2), or Intl.NumberFormat for anything locale-aware.Printing more than a string
console.log takes any number of values of any type, and prints structures rather than calling ToString on them.Option Strict On
Imports System
Imports System.Collections.Generic
Module OutputDemo
Sub Main()
Dim numbers As New List(Of Integer) From {1, 2, 3}
Console.WriteLine(String.Join(", ", numbers))
Console.Write("no newline")
Console.WriteLine(" — then newline")
Console.Error.WriteLine("this goes to stderr")
End Sub
End Moduleconst numbers = [1, 2, 3];
console.log(numbers);
console.log("first", "second", 42, true);
process.stdout.write("no newline");
console.log(" — then newline");
console.error("this goes to stderr");Passing several arguments prints them separated by spaces. Passing an object or array prints its structure —
[ 1, 2, 3 ] — which is far more useful for debugging than .NET's default System.Collections.Generic.List`1. There is no Console.Write; writing without a newline means process.stdout.write in Node. console.error writes to standard error, matching Console.Error.Syntax Fundamentals
Blocks: braces instead of End
Every
End keyword becomes a closing brace, and the condition gains parentheses.Option Strict On
Imports System
Module BlockDemo
Sub Main()
Dim temperature As Integer = 30
If temperature > 25 Then
Console.WriteLine("Warm")
ElseIf temperature > 10 Then
Console.WriteLine("Mild")
Else
Console.WriteLine("Cold")
End If
End Sub
End Moduleconst temperature = 30;
if (temperature > 25) {
console.log("Warm");
} else if (temperature > 10) {
console.log("Mild");
} else {
console.log("Cold");
}The shape is the C family's: parentheses around the condition, braces around the block, no
Then, and else if as two words. Formatting convention puts the opening brace on the same line — this is not arbitrary in JavaScript, and the gotchas section explains why putting it on the next line can silently change what a return does.Semicolons, and the ones you did not type
A statement ends at a semicolon, not at the end of the line — so the continuation underscore disappears.
Option Strict On
Imports System
Module StatementDemo
Sub Main()
Dim total As Integer = 1 + 2 +
3 + 4
Dim label As String = "sum"
Console.WriteLine($"{label}: {total}")
End Sub
End Moduleconst total = 1 + 2 +
3 + 4;
const label = "sum";
console.log(`${label}: ${total}`);Because the semicolon is the terminator, an expression can be broken across as many lines as you like with nothing to mark it. The complication is automatic semicolon insertion: the parser will supply a missing semicolon at a line break, which makes semicolons look optional and mostly lets you omit them. Mostly. A line starting with
( or [ is joined to the previous one instead, which is why many style guides — and the default formatter — put them in. Write them.Comments
Three forms in each language, and the documentation form is the one worth noticing.
Option Strict On
Imports System
Module CommentDemo
''' <summary>Doubles a number.</summary>
Function Twice(value As Integer) As Integer
' A line comment
Return value * 2
End Function
Sub Main()
Console.WriteLine(Twice(21))
End Sub
End Module/** Doubles a number. */
function twice(value: number): number {
// A line comment
/* and a block comment,
across as many lines as you like */
return value * 2;
}
console.log(twice(21));' becomes //, and TypeScript gains a real block comment, /* ... */. The documentation form is JSDoc, /** ... */, and it plays the role '''<summary> plays: editors show it on hover and documentation generators read it. Its tags are @param, @returns, @deprecated — the same ideas as the XML tags, written without angle brackets.Case sensitivity and naming
The same adjustment every other target on this anchor demands.
Option Strict On
Imports System
Module CaseDemo
Sub Main()
Dim customerName As String = "Grace"
' One variable — the editor rewrites your casing
Console.WriteLine(customerName)
Console.WriteLine(CustomerName)
End Sub
End Moduleconst customerName = "Grace";
const CustomerName = "Hopper";
// Two variables, and nothing warns you
console.log(customerName);
console.log(CustomerName);Identifiers are case-sensitive, so
customerName and CustomerName are two names. The conventions differ from .NET's in one important way: methods and properties are camelCase, not PascalCase. PascalCase is reserved for classes, interfaces and type names. So person.Describe() becomes person.describe(), and getting this wrong is the fastest way to make TypeScript code look like it was written by someone visiting from another platform.Variables & Types
Dim becomes let, and mostly const
Two keywords replace
Dim, and the one you should reach for first is the one that forbids reassignment.Option Strict On
Imports System
Module DeclarationDemo
Sub Main()
Dim count As Integer = 10
Dim label As String = "widget"
Const MaximumRetries As Integer = 3
count = 20
Console.WriteLine($"{count} {label} {MaximumRetries}")
End Sub
End Modulelet count: number = 10;
const label: string = "widget";
const MAXIMUM_RETRIES = 3;
count = 20;
// label = "other"; // error: cannot assign to a const
console.log(count, label, MAXIMUM_RETRIES);let declares a variable you intend to reassign; const declares one you do not, and the compiler enforces it. TypeScript convention is const by default and let only where you must — the opposite emphasis from Dim, which has no such distinction. Note that const prevents rebinding, not mutation: a const array can still have items pushed into it. There is a third keyword, var, with older and stranger scoping rules; do not use it.Type annotations and inference
The annotation reads right to left, exactly like
As Integer — and, as with Option Infer, you usually leave it off.Option Strict On
Option Infer On
Imports System
Module AnnotationDemo
Sub Main()
Dim explicitCount As Integer = 10
Dim inferredCount = 10
Console.WriteLine(explicitCount + inferredCount)
End Sub
End Moduleconst explicitCount: number = 10;
const inferredCount = 10; // inferred as number
function add(left: number, right: number): number {
return left + right;
}
console.log(add(explicitCount, inferredCount));value As Integer becomes value: number and a function's return type goes after the parameter list, ): number. TypeScript infers aggressively, so annotating a variable initialized on the same line is redundant and unidiomatic; annotate function parameters and return types, and let everything else infer. Unlike Python's hints, these are checked: the compiler rejects the program if they do not hold. Unlike .NET's, they vanish at runtime, which the gotchas section returns to.There is one numeric type
This is the single most consequential difference for anyone porting business logic, and it is worth reading the output carefully.
Option Strict On
Imports System
Module NumberDemo
Sub Main()
Dim whole As Integer = 7
Dim big As Long = 9007199254740993L
Dim precise As Double = 0.1 + 0.2
Dim exact As Decimal = 0.1D + 0.2D
Console.WriteLine(whole \ 2)
Console.WriteLine(big)
Console.WriteLine(precise)
Console.WriteLine(exact)
End Sub
End Moduleconst whole = 7;
const big = 9007199254740993; // silently wrong
const bigSafe = 9007199254740993n; // bigint literal
const precise = 0.1 + 0.2;
console.log(Math.trunc(whole / 2));
console.log(big);
console.log(bigSafe);
console.log(precise);number is a 64-bit float and there is nothing else — no Integer, no Long, and no Decimal. Integers are exact only up to 2⁵³, which is why big prints a different value than it was given. 0.1 + 0.2 misbehaves exactly as Double does, and there is no decimal type to switch to — so money must not be stored in a number. The two answers are bigint (the n suffix, integers only, no mixing with number) or storing whole cents. Integer division has no operator: Math.trunc(a / b), or Math.floor if you want Python's rounding.Nothing becomes two things
Where Visual Basic has one
Nothing, TypeScript has two absences that mean different things.Option Strict On
Imports System
Module NothingDemo
Sub Main()
Dim missing As String = Nothing
Dim absentNumber As Integer? = Nothing
Console.WriteLine(missing Is Nothing)
Console.WriteLine(absentNumber.HasValue)
End Sub
End Modulelet declared: string | null = null;
let neverSet: string | undefined;
console.log(declared === null);
console.log(neverSet === undefined);
console.log(declared == neverSet); // true — loose equality treats them alike
console.log(neverSet ?? "fallback");null means "deliberately empty" and undefined means "never given a value" — a variable declared and not initialized, a missing object property, a function with no return. Both exist and both are falsy. With strictNullChecks on (the default, and non-negotiable) neither can be assigned to a string: you must widen the type to string | null, which is TypeScript's equivalent of Integer? and covers reference types too. The practical convention is to pick undefined for your own code and let null arrive from JSON and older libraries.Converting between types
The conversion functions have counterparts — and failure is reported by a value rather than an exception or an output parameter.
Option Strict On
Imports System
Module ConversionDemo
Sub Main()
Dim text As String = "123"
Dim parsed As Integer = CInt(text)
Dim asText As String = CStr(parsed * 2)
Dim value As Integer
If Integer.TryParse("12x", value) Then
Console.WriteLine(value)
Else
Console.WriteLine("not a number")
End If
Console.WriteLine($"{parsed} {asText}")
End Sub
End Moduleconst text = "123";
const parsed = Number(text);
const asText = String(parsed * 2);
const value = Number("12x");
if (Number.isNaN(value)) {
console.log("not a number");
} else {
console.log(value);
}
console.log(parsed, asText);CInt/CDbl become Number(), CStr becomes String(), CBool becomes Boolean(). There is no TryParse: a failed conversion produces NaN, a number that is not a number, and it propagates silently through arithmetic — so test with Number.isNaN() at the point of conversion. Two traps: Number("") is 0, not NaN; and parseInt("12x") returns 12, stopping at the first bad character, which is almost never what you want.Truthiness
TypeScript has truthiness, and its list of falsy values contains one entry that regularly surprises people.
Option Strict On
Imports System
Imports System.Collections.Generic
Module TruthDemo
Sub Main()
Dim items As New List(Of String)
Dim text As String = ""
Dim count As Integer = 0
If items.Count = 0 Then Console.WriteLine("no items")
If String.IsNullOrEmpty(text) Then Console.WriteLine("no text")
If count = 0 Then Console.WriteLine("zero")
End Sub
End Moduleconst items: string[] = [];
const text = "";
const count = 0;
if (items.length === 0) console.log("no items");
if (!text) console.log("no text");
if (!count) console.log("zero");
if (items) console.log("an empty array is TRUTHY");Falsy values are
false, 0, NaN, "", null and undefined — and that is the whole list. An empty array is truthy. An empty object is truthy. So if (items) is always true for an array and tells you nothing; the emptiness test is items.length === 0. This is the opposite of Python, where an empty collection is falsy, and it is the most common bug when moving between the two.Operators
Use === and never ==
JavaScript has two equality operators. One of them is a mistake the language cannot remove, and TypeScript inherits it.
Option Strict On
Imports System
Module EqualityDemo
Sub Main()
Dim number As Integer = 1
Dim text As String = "1"
' Option Strict On refuses to compare these at all
Console.WriteLine(number = 1)
Console.WriteLine(text = "1")
Console.WriteLine(number.ToString() = text)
End Sub
End Moduleconst numberValue: number = 1;
const textValue: string = "1";
console.log(numberValue === 1);
console.log(textValue === "1");
console.log(String(numberValue) === textValue);
// Loose equality converts before comparing:
console.log((1 as unknown) == "1"); // true
console.log((0 as unknown) == ""); // true
console.log(([] as unknown) == false); // true=== compares value and type and is the one to use, always. == converts its operands first, using rules nobody remembers, which is how 0 == "" and [] == false come out true. Inequality is !== and !=, the same split. TypeScript protects you where it can — comparing a number to a string with == is a compile error, which is why the examples above have to launder the types through unknown — but any value typed any slips past. Configure your linter to ban == outright, with the one exception x == null, which usefully catches both null and undefined.Logical operators
The short-circuiting pair translates directly — and then does something Visual Basic's never does.
Option Strict On
Imports System
Module LogicalDemo
Sub Main()
Dim age As Integer = 30
Dim member As Boolean = True
If age > 18 AndAlso member Then Console.WriteLine("eligible")
If age < 18 OrElse member Then Console.WriteLine("either")
If Not member Then Console.WriteLine("not a member")
End Sub
End Moduleconst age = 30;
const member = true;
if (age > 18 && member) console.log("eligible");
if (age < 18 || member) console.log("either");
if (!member) console.log("not a member");
// && and || return an OPERAND, not a boolean
console.log(0 || "fallback");
console.log("value" && "second");AndAlso becomes &&, OrElse becomes ||, Not becomes !. The twist is that they do not return booleans: || returns its first truthy operand and && its last, which is why 0 || "fallback" is the string. That makes || a fallback operator, and a buggy one — it replaces 0 and "" as well as null. Use ?? instead when only null and undefined should trigger the fallback. Plain And and Or have no equivalent; & and | are bitwise and, worse, truncate to 32 bits.Arithmetic, division and exponent
One of the four operators survives unchanged, one changes symbol, and one has to become a function call.
Option Strict On
Imports System
Module ArithmeticDemo
Sub Main()
Dim quotient As Integer = 17 \ 5
Dim exact As Double = 17 / 5
Dim remainder As Integer = 17 Mod 5
Dim squared As Double = 7 ^ 2
Console.WriteLine($"{quotient} {exact} {remainder} {squared}")
End Sub
End Moduleconst quotient = Math.trunc(17 / 5);
const exact = 17 / 5;
const remainder = 17 % 5;
const squared = 7 ** 2;
console.log(quotient, exact, remainder, squared);/ is always floating-point, as in Visual Basic — there is no integer division operator, so \ becomes Math.trunc(a / b). Mod becomes %, and like Visual Basic (and unlike Python) it takes the sign of the left operand. ^ becomes **; writing ^ gives you bitwise exclusive-or, so 7 ^ 2 silently evaluates to 5. TypeScript also has ++ and --, which Visual Basic never had.If() becomes ?? and ?.
Both halves of Visual Basic's
If() get their own operator, and a third arrives that has no counterpart.Option Strict On
Imports System
Module NullishDemo
Sub Main()
Dim supplied As String = Nothing
Dim label As String = If(supplied, "(unnamed)")
Dim score As Integer = 72
Dim grade As String = If(score >= 60, "pass", "fail")
Console.WriteLine($"{label} / {grade}")
End Sub
End Moduleconst supplied: string | null = null;
const label = supplied ?? "(unnamed)";
const score = 72;
const grade = score >= 60 ? "pass" : "fail";
const person: { address?: { city?: string } } = {};
console.log(label, grade, person.address?.city ?? "no city");Two-argument
If(value, fallback) becomes ??, which falls back only on null and undefined — unlike ||, it leaves 0 and "" alone. Three-argument If(condition, a, b) becomes condition ? a : b. The new one is optional chaining, ?.: if the thing to its left is null or undefined the whole expression short-circuits to undefined instead of throwing, which replaces the nested null checks that dominate this kind of code in .NET. It works on calls and indexes too — list?.[0], callback?.().Joining strings
The
& becomes + — and the last two lines show exactly why Visual Basic gave concatenation its own operator.Option Strict On
Imports System
Module ConcatDemo
Sub Main()
Dim first As String = "Grace"
Dim last As String = "Hopper"
Console.WriteLine(first & " " & last)
Console.WriteLine("Answer: " & 42)
End Sub
End Moduleconst first = "Grace";
const last = "Hopper";
console.log(first + " " + last);
console.log(`${first} ${last}`);
console.log("Answer: " + 42);
console.log(1 + 2 + " items");
console.log("items: " + 1 + 2);+ means both addition and concatenation, resolved left to right by the types it meets. 1 + 2 + " items" adds first and gives "3 items"; "items: " + 1 + 2 concatenates first and gives "items: 12". This is precisely the ambiguity & exists to avoid, and it is why a template literal is the better habit for anything with more than two parts. TypeScript will not let you add a number to an object, but string-plus-number is legal and does what you see.Strings
Common string operations
The same operations, renamed to
camelCase, with one that behaves differently than its name suggests.Option Strict On
Imports System
Module StringMethodDemo
Sub Main()
Dim text As String = " Visual Basic "
Console.WriteLine($"[{text.Trim()}]")
Console.WriteLine(text.Trim().ToUpper())
Console.WriteLine(text.Contains("Basic"))
Console.WriteLine(text.Trim().Replace(" ", "-"))
Console.WriteLine(text.Trim().StartsWith("Visual"))
Console.WriteLine(text.Trim().Length)
End Sub
End Moduleconst text = " Visual Basic ";
console.log(`[${text.trim()}]`);
console.log(text.trim().toUpperCase());
console.log(text.includes("Basic"));
console.log(text.trim().replaceAll(" ", "-"));
console.log(text.trim().startsWith("Visual"));
console.log(text.trim().length);Trim→trim, ToUpper→toUpperCase, Contains→includes, StartsWith→startsWith. Length becomes length — a property, no parentheses, unlike Python's len(). The trap is replace: given a string it replaces only the first occurrence, unlike .NET's Replace. replaceAll is what you actually want, and it is what the anchor column's Replace means.Substrings
One method covers
Substring, Left and Right, because it accepts negative positions.Option Strict On
Imports System
Module SubstringDemo
Sub Main()
Dim text As String = "Visual Basic"
Console.WriteLine(text.Substring(0, 6))
Console.WriteLine(text.Substring(7))
Console.WriteLine(text.Substring(text.Length - 5))
Console.WriteLine(text(0))
Console.WriteLine(text.IndexOf("Basic"))
End Sub
End Moduleconst text = "Visual Basic";
console.log(text.slice(0, 6));
console.log(text.slice(7));
console.log(text.slice(-5));
console.log(text[0]);
console.log(text.indexOf("Basic"));slice(start, end) takes an end index where Substring(start, length) takes a length — so Substring(0, 6) and slice(0, 6) agree here by coincidence, and stop agreeing the moment the start is not zero. Negative numbers count from the end, so slice(-5) is Right(text, 5). There is an older substring method with subtly different behavior for negative arguments; prefer slice. Indexing uses square brackets and yields a one-character string — there is no character type.Multi-line and quoted strings
Two interchangeable quote characters, a backslash that escapes, and a template literal that spans lines.
Option Strict On
Imports System
Module QuotingDemo
Sub Main()
Dim quoted As String = "She said ""hello""."
Dim path As String = "C:\reports\summary.txt"
Dim block As String = "line one" & Environment.NewLine & "line two"
Console.WriteLine(quoted)
Console.WriteLine(path)
Console.WriteLine(block)
End Sub
End Moduleconst quoted = 'She said "hello".';
const path = "C:\\reports\\summary.txt";
const block = `line one
line two`;
console.log(quoted);
console.log(path);
console.log(block);Single and double quotes mean the same thing, so the easy way to include a quote is to wrap in the other kind. The backslash escapes, as in C#, so a Windows path needs each one doubled — and there is no verbatim-string prefix: TypeScript has no
@"..." and no r"...". A template literal preserves newlines, which is what replaces the & Environment.NewLine & chain, and it interpolates too.Splitting and joining
Splitting is a rename; joining moves from a static method to a method on the array.
Option Strict On
Imports System
Module SplitDemo
Sub Main()
Dim line As String = "red,green,blue"
Dim parts() As String = line.Split(","c)
Console.WriteLine(parts.Length)
Console.WriteLine(parts(1))
Console.WriteLine(String.Join(" | ", parts))
End Sub
End Moduleconst line = "red,green,blue";
const parts = line.split(",");
console.log(parts.length);
console.log(parts[1]);
console.log(parts.join(" | "));Split becomes split and takes a string (or a regular expression). String.Join(separator, parts) becomes parts.join(separator) — the array does the joining, which is the reverse of .NET and the same shape Python uses, only with the roles the other way round again. Note that join with no argument uses a comma, not an empty string.Arrays, Objects & Maps
Arrays replace both arrays and List(Of T)
One growable type does the work of arrays,
List(Of T) and the VB6 Collection.Option Strict On
Imports System
Imports System.Collections.Generic
Module ArrayDemo
Sub Main()
Dim fruits As New List(Of String) From {"apple", "banana"}
fruits.Add("cherry")
fruits.Insert(0, "apricot")
fruits.RemoveAt(1)
Console.WriteLine(fruits.Count)
Console.WriteLine(fruits(0))
Console.WriteLine(String.Join(", ", fruits))
End Sub
End Moduleconst fruits: string[] = ["apple", "banana"];
fruits.push("cherry");
fruits.unshift("apricot");
fruits.splice(1, 1);
console.log(fruits.length);
console.log(fruits[0]);
console.log(fruits.join(", "));List(Of String) becomes string[] (or Array<string> — the same type, two spellings). It grows and shrinks freely, so there is no fixed-size array to choose. Add becomes push, Insert(0, x) becomes unshift, RemoveAt becomes splice(index, 1), and Count becomes length. Note that const does not prevent any of these: it forbids reassigning fruits, not changing what is in it.Array methods replace LINQ
Every LINQ operator you use daily has an array method — under a different name, and evaluated immediately.
Option Strict On
Imports System
Imports System.Linq
Module LinqDemo
Sub Main()
Dim numbers() As Integer = {5, 3, 9, 1, 7, 2}
Dim result = numbers.
Where(Function(number) number > 2).
Select(Function(number) number * 10).
ToList()
Console.WriteLine(String.Join(", ", result))
Console.WriteLine(numbers.Sum())
Console.WriteLine(numbers.Any(Function(number) number > 8))
Console.WriteLine(numbers.FirstOrDefault(Function(number) number > 6))
End Sub
End Moduleconst numbers = [5, 3, 9, 1, 7, 2];
const result = numbers
.filter((number) => number > 2)
.map((number) => number * 10);
console.log(result);
console.log(numbers.reduce((total, number) => total + number, 0));
console.log(numbers.some((number) => number > 8));
console.log(numbers.find((number) => number > 6));Where→filter, Select→map, Any→some, All→every, FirstOrDefault→find, Count→length, Contains→includes, Aggregate→reduce. There is no Sum, Min or Max on arrays: sum is a reduce, and min and max are Math.min(...numbers). The behavioral difference is laziness — LINQ builds a query and runs it when enumerated, while every array method here builds a whole new array immediately, so a long chain over a large array allocates at each step.Object literals
A structure with named fields needs no class and no declaration — you write the value and TypeScript works out the type.
Option Strict On
Imports System
Public Class Person
Public Property Name As String = ""
Public Property Age As Integer
End Class
Module ObjectDemo
Sub Main()
Dim person As New Person With {.Name = "Ada", .Age = 36}
Console.WriteLine(person.Name)
person.Age = 37
Console.WriteLine(person.Age)
End Sub
End Moduleconst person = { name: "Ada", age: 36 };
console.log(person.name);
person.age = 37;
console.log(person.age);
console.log(person["name"]);
console.log(Object.keys(person));An object literal is
{ key: value }, and its type is inferred as { name: string; age: number }. This is the workhorse of TypeScript: most data is passed around as object literals rather than class instances, and a class is reserved for something with behavior. Members are reached with a dot or with brackets, and Object.keys, Object.values and Object.entries let you walk them — an object doubles as a string-keyed dictionary, which the next row builds on.Dictionaries: Record and Map
There are two answers, and choosing the wrong one is a common source of trouble.
Option Strict On
Imports System
Imports System.Collections.Generic
Module DictionaryDemo
Sub Main()
Dim ages As New Dictionary(Of String, Integer) From {
{"Ada", 36},
{"Grace", 45}
}
ages("Alan") = 41
For Each entry As KeyValuePair(Of String, Integer) In ages
Console.WriteLine($"{entry.Key} is {entry.Value}")
Next
Console.WriteLine(ages.ContainsKey("Ada"))
End Sub
End Moduleconst ages = new Map<string, number>([
["Ada", 36],
["Grace", 45],
]);
ages.set("Alan", 41);
for (const [name, age] of ages) {
console.log(`${name} is ${age}`);
}
console.log(ages.has("Ada"));
console.log(ages.get("Nobody") ?? 0);
const record: Record<string, number> = { Ada: 36 };
console.log(record["Ada"]);Map is the real Dictionary(Of K, V): any key type, insertion order preserved, set/get/has/delete, and a size property. Record<string, number> is a plain object used as a lookup — convenient, and what JSON gives you, but its keys can only be strings, and it inherits members from Object.prototype, so a key called "constructor" or "toString" will surprise you. ContainsKey→has, TryGetValue→get plus ??. Iterating a Map yields [key, value] pairs, destructured in the loop header.Destructuring and spread
Two features that appear on nearly every line of real TypeScript, and have no Visual Basic counterpart.
Option Strict On
Imports System
Module DestructureDemo
Function MinimumAndMaximum(values() As Integer) As (Smallest As Integer, Largest As Integer)
Dim smallest As Integer = values(0)
Dim largest As Integer = values(0)
For Each value As Integer In values
If value < smallest Then smallest = value
If value > largest Then largest = value
Next
Return (smallest, largest)
End Function
Sub Main()
Dim result = MinimumAndMaximum(New Integer() {4, 9, 1, 7})
Console.WriteLine($"{result.Smallest}..{result.Largest}")
End Sub
End Modulefunction minimumAndMaximum(values: number[]): [number, number] {
return [Math.min(...values), Math.max(...values)];
}
const [low, high] = minimumAndMaximum([4, 9, 1, 7]);
console.log(`${low}..${high}`);
const person = { name: "Ada", age: 36, city: "London" };
const { name, ...rest } = person;
console.log(name, rest);
const combined = [...[1, 2], ...[3, 4]];
console.log(combined);Destructuring pulls parts out of an array or object into named variables in one statement —
const [low, high] = ... for arrays by position, const { name } = ... for objects by key. Spread, ..., does the reverse: it expands an array into arguments (Math.min(...values)) or copies elements and properties into a new array or object. ...rest on the left collects whatever was not named. The return type [number, number] is a tuple type, TypeScript's fixed-length array; unlike Visual Basic's tuples, the elements can also be named for readability — [low: number, high: number].Control Flow
Select Case becomes switch
The shape survives, and the missing keyword at the end of each branch is a real hazard here.
Option Strict On
Imports System
Module SelectDemo
Sub Main()
Dim code As Integer = 3
Select Case code
Case 1
Console.WriteLine("one")
Case 2, 3
Console.WriteLine("two or three")
Case Else
Console.WriteLine("something else")
End Select
End Sub
End Moduleconst code = 3;
switch (code) {
case 1:
console.log("one");
break;
case 2:
case 3:
console.log("two or three");
break;
default:
console.log("something else");
}Each branch needs an explicit
break, and unlike C#, TypeScript does not make you write one — omitting it falls through into the next branch and runs it too. That is occasionally useful and far more often a bug, so turn on the compiler's noFallthroughCasesInSwitch option and let it catch them. Stacking bare case labels, as with 2 and 3 above, is the deliberate kind of fall-through and is always allowed. There is no Case Is > and no Case 4 To 6; ranges become an if chain or switch (true).Testing a type narrows it
The test and the conversion collapse into one, because the compiler remembers what you proved.
Option Strict On
Imports System
Module NarrowDemo
Sub Describe(value As Object)
If TypeOf value Is Integer Then
Dim number = CInt(value)
Console.WriteLine($"number {number * 2}")
ElseIf TypeOf value Is String Then
Dim text = CStr(value)
Console.WriteLine($"string of {text.Length}")
End If
End Sub
Sub Main()
Describe(21)
Describe("hello")
End Sub
End Modulefunction describe(value: number | string): void {
if (typeof value === "number") {
console.log(`number ${value * 2}`);
} else {
console.log(`string of ${value.length}`);
}
}
describe(21);
describe("hello");TypeOf value Is Integer becomes typeof value === "number", and inside that branch the compiler narrows value from number | string to number — so value * 2 type-checks with no cast, and in the else branch value.length does too. This is control-flow analysis, and it is TypeScript's best feature: if (x === null) return; narrows away the null for the rest of the function. For class instances the test is instanceof; typeof only knows the primitive kinds.A switch the compiler checks
No
Case Else, and no missing-branch bug — because the type says exactly which values exist.Option Strict On
Imports System
Module ExhaustiveDemo
Enum Status
Pending
Active
Closed
End Enum
Function Describe(value As Status) As String
Select Case value
Case Status.Pending : Return "waiting"
Case Status.Active : Return "running"
Case Status.Closed : Return "done"
Case Else : Return "unknown"
End Select
End Function
Sub Main()
Console.WriteLine(Describe(Status.Active))
Console.WriteLine(Describe(Status.Closed))
End Sub
End Moduletype Status = "pending" | "active" | "closed";
function describe(value: Status): string {
switch (value) {
case "pending": return "waiting";
case "active": return "running";
case "closed": return "done";
}
}
console.log(describe("active"));
console.log(describe("closed"));A union of literal types lists the permitted values, and the compiler knows the
switch covers all of them — which is why the function type-checks with no fallback branch and no return after the switch. Add a fourth status and every unhandled switch becomes a compile error. A Visual Basic Enum cannot do this: it is an Integer underneath, any number can be cast into it, and so Case Else is always required.Loops
For ... Next
A declarative range becomes three explicit clauses, exactly as in C#.
Option Strict On
Imports System
Module ForDemo
Sub Main()
For index As Integer = 1 To 5
Console.Write(index & " ")
Next
Console.WriteLine()
For countdown As Integer = 10 To 0 Step -2
Console.Write(countdown & " ")
Next
Console.WriteLine()
End Sub
End Modulelet line = "";
for (let index = 1; index <= 5; index++) {
line += index + " ";
}
console.log(line);
line = "";
for (let countdown = 10; countdown >= 0; countdown -= 2) {
line += countdown + " ";
}
console.log(line);The clauses are "initializer; condition; step". You state the ending condition rather than an ending value, so the inclusive
To 5 becomes <= 5 — the classic off-by-one. Use let, not const, for the loop variable, since it is reassigned each pass. Step -2 becomes the step clause. Note the examples build a string rather than calling Console.Write: there is no partial-line print in a portable form, so accumulating and logging once is the idiom.For Each becomes for ... of
Note the
of — the other spelling of this loop iterates something completely different.Option Strict On
Imports System
Imports System.Collections.Generic
Module ForEachDemo
Sub Main()
Dim words As New List(Of String) From {"alpha", "beta", "gamma"}
For Each word As String In words
Console.WriteLine(word.ToUpper())
Next
For index As Integer = 0 To words.Count - 1
Console.WriteLine($"{index}: {words(index)}")
Next
End Sub
End Moduleconst words = ["alpha", "beta", "gamma"];
for (const word of words) {
console.log(word.toUpperCase());
}
for (const [index, word] of words.entries()) {
console.log(`${index}: ${word}`);
}
words.forEach((word, index) => console.log(index, word));For Each x In items becomes for (const x of items). Getting the index too is items.entries(), which yields [index, value] pairs, or the forEach method, whose callback receives both. 🚨 for ... in is not this loop. It iterates an object's keys, and over an array it gives you the indices as strings — "0", "1" — plus anything else attached to the object. Using it on an array is almost always a bug.While and Do loops
Both loop forms have direct counterparts — the only work is inverting an
Until.Option Strict On
Imports System
Module WhileDemo
Sub Main()
Dim remaining As Integer = 3
While remaining > 0
Console.WriteLine($"remaining {remaining}")
remaining -= 1
End While
Dim attempt As Integer = 0
Do
attempt += 1
Console.WriteLine($"attempt {attempt}")
Loop Until attempt >= 2
End Sub
End Modulelet remaining = 3;
while (remaining > 0) {
console.log(`remaining ${remaining}`);
remaining -= 1;
}
let attempt = 0;
do {
attempt += 1;
console.log(`attempt ${attempt}`);
} while (attempt < 2);While ... End While becomes while (...) { }, and the Do ... Loop family becomes do { } while (...). There is no Until, so Loop Until attempt >= 2 has to be flipped to while (attempt < 2). Note the semicolon after the closing while of a do loop — it is the one place a brace is followed by one.Leaving a loop early
The two keywords lose the word naming the construct — and gain the ability to name a specific loop.
Option Strict On
Imports System
Module BreakDemo
Sub Main()
For number As Integer = 1 To 10
If number Mod 2 = 0 Then Continue For
If number > 7 Then Exit For
Console.WriteLine(number)
Next
End Sub
End Moduleouter:
for (let number = 1; number <= 10; number++) {
if (number % 2 === 0) continue;
if (number > 7) break outer;
console.log(number);
}Exit For becomes break and Continue For becomes continue, both acting on the innermost loop by default. TypeScript adds labels: naming a loop lets break outer or continue outer act on an enclosing one, which is what you need to leave a nested pair in one step. Visual Basic has no equivalent and neither does C#, where the answer is goto or extracting a method. Exit Sub and Exit Function both become return.Functions
Sub and Function both become function
One keyword covers both, and the return type moves to the end of the signature.
Option Strict On
Imports System
Module FunctionDemo
Sub Announce(message As String)
Console.WriteLine($"** {message} **")
End Sub
Function Add(left As Integer, right As Integer) As Integer
Return left + right
End Function
Sub Main()
Announce("starting")
Console.WriteLine(Add(2, 3))
End Sub
End Modulefunction announce(message: string): void {
console.log(`** ${message} **`);
}
function add(left: number, right: number): number {
return left + right;
}
announce("starting");
console.log(add(2, 3));Sub becomes function ... : void and Function ... As Integer becomes function ... : number. The return type is usually inferred, so writing it is optional — but writing it on exported functions is good practice, because it makes the compiler check the body against your intent rather than inferring whatever the body happens to produce. Unlike Python, a function declared with function is hoisted: it can be called on a line above its definition.Lambdas become arrow functions
One arrow replaces both
Function(...) and Sub(...), and there is no delegate type to declare.Option Strict On
Imports System
Imports System.Collections.Generic
Module LambdaDemo
Sub Main()
Dim twice As Func(Of Integer, Integer) = Function(value) value * 2
Dim shout As Action(Of String) =
Sub(message)
Console.WriteLine(message.ToUpper())
End Sub
Console.WriteLine(twice(21))
shout("done")
End Sub
End Moduleconst twice = (value: number): number => value * 2;
const shout = (message: string): void => {
console.log(message.toUpperCase());
};
console.log(twice(21));
shout("done");Function(value) value * 2 becomes (value) => value * 2; a body in braces needs an explicit return. There is no Func(Of ...) or Action(Of ...) to name — a function's type is written as a signature, (value: number) => number, and is usually inferred anyway. Arrow functions are what you pass to map, filter and every callback, and they have one behavioral difference from function that matters enormously: how they treat this, covered in the gotchas.Optional and default parameters
Default values translate directly. Named arguments do not exist at all, and the replacement is worth learning early.
Option Strict On
Imports System
Module ArgumentDemo
Function Greet(name As String,
Optional greeting As String = "Hello",
Optional punctuation As String = "!") As String
Return $"{greeting}, {name}{punctuation}"
End Function
Sub Main()
Console.WriteLine(Greet("Ada"))
Console.WriteLine(Greet("Grace", "Welcome"))
Console.WriteLine(Greet("Alan", punctuation:="."))
End Sub
End Modulefunction greet(
name: string,
greeting = "Hello",
punctuation = "!",
): string {
return `${greeting}, ${name}${punctuation}`;
}
console.log(greet("Ada"));
console.log(greet("Grace", "Welcome"));
console.log(greet("Alan", undefined, "."));
// Named arguments do not exist — an options object is the idiom
function greetWith({ name, punctuation = "!" }: { name: string; punctuation?: string }) {
return `Hello, ${name}${punctuation}`;
}
console.log(greetWith({ name: "Alan", punctuation: "." }));A default value makes a parameter optional, and
Optional disappears; a parameter that may be omitted with no default is marked name?: string and arrives as undefined. What TypeScript has no equivalent for is punctuation:= — to skip a middle argument you must pass undefined positionally. The idiomatic answer is to take a single options object and destructure it in the parameter list, which gives you named arguments, defaults and any order, and is what almost every TypeScript library does.ParamArray becomes a rest parameter
A direct translation, using the same three dots as spread — because it is the same idea running backwards.
Option Strict On
Imports System
Module RestDemo
Function SumAll(ParamArray values() As Integer) As Integer
Dim total As Integer = 0
For Each value As Integer In values
total += value
Next
Return total
End Function
Sub Main()
Console.WriteLine(SumAll(1, 2, 3))
Console.WriteLine(SumAll())
Console.WriteLine(SumAll(New Integer() {4, 5}))
End Sub
End Modulefunction sumAll(...values: number[]): number {
return values.reduce((total, value) => total + value, 0);
}
console.log(sumAll(1, 2, 3));
console.log(sumAll());
console.log(sumAll(...[4, 5]));ParamArray values() As Integer becomes ...values: number[], must be last, and collects the extra arguments into a real array. Passing a ready-made array is sumAll(...[4, 5]) — the spread operator again, this time unpacking rather than collecting. Note reduce's second argument, 0: it is the starting value, and omitting it on an empty array throws.There is no ByRef
Every argument is passed the same way, and what happens next depends on what kind of value it is.
Option Strict On
Imports System
Imports System.Collections.Generic
Module ByRefDemo
Sub Twice(ByRef value As Integer)
value *= 2
End Sub
Sub AddItem(items As List(Of Integer))
items.Add(99)
End Sub
Sub Main()
Dim number As Integer = 21
Twice(number)
Console.WriteLine(number)
Dim numbers As New List(Of Integer) From {1}
AddItem(numbers)
Console.WriteLine(numbers.Count)
End Sub
End Modulefunction twice(value: number): number {
value *= 2; // the caller sees nothing
return value;
}
function addItem(items: number[]): void {
items.push(99); // mutates the caller's array
}
let number = 21;
twice(number);
console.log(number);
number = twice(number);
console.log(number);
const numbers = [1];
addItem(numbers);
console.log(numbers.length);There is no
ByRef, no ref and no out. Assigning to a parameter changes only the local name. But arrays and objects are references, so calling a mutating method on one changes what the caller holds — the same "rebinding versus mutating" rule Python has. The idiomatic answer is to return a new value rather than modify an argument, and TypeScript can help you enforce it: annotate the parameter readonly number[] and the compiler rejects the push.The Type System
Interfaces are structural
The single largest conceptual difference in TypeScript's type system: an interface describes a shape, not a relationship you declare.
Option Strict On
Imports System
Imports System.Collections.Generic
Public Interface IGreeter
Function Greet(name As String) As String
End Interface
Public Class Formal
Implements IGreeter
Public Function Greet(name As String) As String Implements IGreeter.Greet
Return $"Good day, {name}."
End Function
End Class
Module InterfaceDemo
Sub Main()
Dim greeters As New List(Of IGreeter) From {New Formal()}
For Each greeter As IGreeter In greeters
Console.WriteLine(greeter.Greet("Ada"))
Next
End Sub
End Moduleinterface Greeter {
greet(name: string): string;
}
// No "implements" needed — the shape is the contract
const formal = {
greet: (name: string) => `Good day, ${name}.`,
};
const casual = {
greet: (name: string) => `Hi ${name}!`,
extra: "ignored",
};
const greeters: Greeter[] = [formal, casual];
for (const greeter of greeters) {
console.log(greeter.greet("Ada"));
}Nothing declares that
formal implements Greeter. It has a greet method with the right signature, so it is a Greeter — this is structural typing, and it is why TypeScript can describe data that already exists, including JSON off the wire, without owning the types. Extra members are fine, as casual shows. A class may still write implements Greeter, and it is worth doing for the error message, but it changes nothing about assignability. Note also that interface names do not take an I prefix in TypeScript convention.Union types and type aliases
A type can be "one of these" — including "one of these exact values" — and there is nothing like it in .NET.
Option Strict On
Imports System
Module UnionDemo
' Visual Basic has no union type — the nearest thing is Object
' plus a runtime test, or a hand-written wrapper class
Function Describe(value As Object) As String
If TypeOf value Is Integer Then Return $"number {value}"
If TypeOf value Is String Then Return $"text {value}"
Return "unknown"
End Function
Sub Main()
Console.WriteLine(Describe(42))
Console.WriteLine(Describe("hello"))
Console.WriteLine(Describe(3.5))
End Sub
End Moduletype Identifier = number | string;
type Status = "pending" | "active" | "closed";
function describe(value: Identifier): string {
return typeof value === "number" ? `number ${value}` : `text ${value}`;
}
console.log(describe(42));
console.log(describe("hello"));
const status: Status = "active";
// const wrong: Status = "finished"; // compile error
console.log(status);number | string is a union type: a value that is one or the other, with the compiler forcing you to narrow before using either. "pending" | "active" | "closed" is a union of literal types, which does the job of a Visual Basic Enum but is genuinely closed — no cast can smuggle another value in. type X = ... gives a name to any type, not just object shapes. The anchor column shows the nearest Visual Basic equivalent, and the difference in safety is the point: Object accepts everything and checks nothing.Generics
The same idea with the same angle brackets —
(Of T) is the only thing that changes shape.Option Strict On
Imports System
Imports System.Collections.Generic
Module GenericDemo
Function FirstOrFallback(Of T)(items As List(Of T), fallback As T) As T
If items.Count = 0 Then Return fallback
Return items(0)
End Function
Sub Main()
Console.WriteLine(FirstOrFallback(New List(Of Integer) From {1, 2}, -1))
Console.WriteLine(FirstOrFallback(New List(Of String), "(empty)"))
End Sub
End Modulefunction firstOrFallback<T>(items: T[], fallback: T): T {
return items.length === 0 ? fallback : items[0];
}
console.log(firstOrFallback([1, 2], -1));
console.log(firstOrFallback<string>([], "(empty)"));
interface Box<T> {
value: T;
}
const boxed: Box<number> = { value: 7 };
console.log(boxed.value);Function Name(Of T)(...) becomes function name<T>(...), and List(Of T) becomes T[] or Array<T>. Type arguments are usually inferred from the call, as the first line shows. Constraints exist too: Where T As IComparable becomes <T extends Comparable>. The deep difference is erasure — a .NET generic is reified, so List(Of Integer) really stores integers and can be inspected at runtime, while a TypeScript generic is gone after compilation and exists only to check your code.readonly and as const
The
ReadOnly keyword survives, applies to arrays as well, and gains a companion that freezes literal types.Option Strict On
Imports System
Public Class Settings
Public ReadOnly Property Retries As Integer
Public Sub New(retries As Integer)
Me.Retries = retries
End Sub
End Class
Module ReadOnlyDemo
Sub Main()
Dim settings As New Settings(3)
Console.WriteLine(settings.Retries)
' settings.Retries = 5 ' would not compile
End Sub
End Moduleinterface Settings {
readonly retries: number;
readonly hosts: readonly string[];
}
const settings: Settings = { retries: 3, hosts: ["a", "b"] };
console.log(settings.retries, settings.hosts.length);
// settings.retries = 5; // compile error
// settings.hosts.push("c"); // compile error
const STATUSES = ["pending", "active"] as const;
console.log(STATUSES[0]);readonly on a property forbids assignment after construction, exactly as ReadOnly does. readonly string[] goes further than .NET: it removes the mutating methods from the type, so push and splice become compile errors. as const takes a literal and makes everything in it readonly and narrows each value to its literal type — so STATUSES has type readonly ["pending", "active"] rather than string[], which is how a list of allowed values and the union type describing them stay in step. All of this is compile-time only; nothing is frozen at runtime.Object becomes unknown, not any
Two escape hatches from the type system, and only one of them is safe.
Option Strict On
Imports System
Module ObjectDemo
Sub Main()
Dim value As Object = "hello"
' Option Strict On forces the conversion to be explicit
Dim text As String = CStr(value)
Console.WriteLine(text.ToUpper())
End Sub
End Moduleconst loose: any = "hello";
console.log(loose.toUpperCase());
console.log(loose.noSuchMethod === undefined); // any silences everything
const safe: unknown = "hello";
if (typeof safe === "string") {
console.log(safe.toUpperCase());
}any switches type checking off for that value — every property access, call and assignment is permitted and nothing is checked, which is how a strongly-typed codebase quietly stops being one. unknown is the honest version: it accepts any value but permits nothing until you narrow it, which is what Object plus Option Strict On effectively gives you. Use unknown for data off the wire, and turn on noImplicitAny so the compiler tells you where any crept in by omission.Classes & Objects
A class and its constructor
The constructor gets a fixed name, and TypeScript offers a shorthand that declares the fields and assigns them in one place.
Option Strict On
Imports System
Public Class Person
Private ReadOnly _name As String
Private ReadOnly _age As Integer
Public Sub New(name As String, age As Integer)
_name = name
_age = age
End Sub
Public Function Describe() As String
Return $"{_name}, age {_age}"
End Function
End Class
Module ClassDemo
Sub Main()
Dim person As New Person("Ada", 36)
Console.WriteLine(person.Describe())
End Sub
End Moduleclass Person {
constructor(
private readonly name: string,
private readonly age: number,
) {}
describe(): string {
return `${this.name}, age ${this.age}`;
}
}
const person = new Person("Ada", 36);
console.log(person.describe());Public Sub New becomes constructor, and Me becomes this — which, unlike Me, is required for every field access. The shorthand shown is a TypeScript-only feature called parameter properties: an access modifier on a constructor parameter declares the field and assigns it, replacing the two declarations and two assignments the anchor column needs. New Person(...) becomes new Person(...), lower case.Properties
Properties exist, split into two keywords — and privacy comes in two flavors, only one of which is real.
Option Strict On
Imports System
Public Class Temperature
Private _celsius As Double
Public Property Celsius As Double
Get
Return _celsius
End Get
Set(value As Double)
_celsius = Math.Max(value, -273.15)
End Set
End Property
Public ReadOnly Property Fahrenheit As Double
Get
Return _celsius * 9.0 / 5.0 + 32
End Get
End Property
End Class
Module PropertyDemo
Sub Main()
Dim reading As New Temperature()
reading.Celsius = 100.0
Console.WriteLine(reading.Fahrenheit)
reading.Celsius = -500
Console.WriteLine(reading.Celsius)
End Sub
End Moduleclass Temperature {
#celsius = 0;
get celsius(): number {
return this.#celsius;
}
set celsius(value: number) {
this.#celsius = Math.max(value, -273.15);
}
get fahrenheit(): number {
return this.#celsius * 9.0 / 5.0 + 32;
}
}
const reading = new Temperature();
reading.celsius = 100.0;
console.log(reading.fahrenheit);
reading.celsius = -500;
console.log(reading.celsius);Property ... Get/Set becomes separate get and set accessors sharing a name; a get with no set is ReadOnly Property. The #celsius field is a true private field, enforced at runtime and invisible from outside. TypeScript's own private keyword is checked only at compile time and is plain and visible in the emitted JavaScript, so # is the stronger choice when the object crosses a boundary. As in C#, the value parameter of the setter is written out explicitly.Inheritance and overriding
Four Visual Basic keywords map onto three TypeScript ones, and one of them is optional in a way that matters.
Option Strict On
Imports System
Public MustInherit Class Shape
Public MustOverride Function Area() As Double
Public Overridable Function Describe() As String
Return $"{Me.GetType().Name}: {Area():F2}"
End Function
End Class
Public Class Circle
Inherits Shape
Private ReadOnly _radius As Double
Public Sub New(radius As Double)
_radius = radius
End Sub
Public Overrides Function Area() As Double
Return Math.PI * _radius * _radius
End Function
End Class
Module InheritanceDemo
Sub Main()
Dim shape As Shape = New Circle(2.0)
Console.WriteLine(shape.Describe())
End Sub
End Moduleabstract class Shape {
abstract area(): number;
describe(): string {
return `${this.constructor.name}: ${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private readonly radius: number) {
super();
}
override area(): number {
return Math.PI * this.radius * this.radius;
}
}
const shape: Shape = new Circle(2.0);
console.log(shape.describe());Inherits becomes extends, MustInherit and MustOverride both become abstract, and MyBase becomes super. Overridable has no equivalent: every method can be overridden. Overrides becomes override, which is optional unless you turn on noImplicitOverride — and you should, because without it a misspelled method name silently adds a new method rather than replacing one. There is no NotInheritable/sealed for classes.Shared becomes static
A keyword swap — and a note on the construct you have been using instead of a class.
Option Strict On
Imports System
Public Class Counter
Private Shared _total As Integer = 0
Public Shared Sub Increment()
_total += 1
End Sub
Public Shared ReadOnly Property Total As Integer
Get
Return _total
End Get
End Property
End Class
Module SharedDemo
Sub Main()
Counter.Increment()
Counter.Increment()
Console.WriteLine(Counter.Total)
End Sub
End Moduleclass Counter {
static #total = 0;
static increment(): void {
Counter.#total += 1;
}
static get total(): number {
return Counter.#total;
}
}
Counter.increment();
Counter.increment();
console.log(Counter.total);Shared becomes static, with the same meaning. What has no counterpart is the Module, whose members can be called without naming it. A class of static members works, but the idiomatic TypeScript answer to "a bag of related functions" is not a class at all — it is a module: a file of exported functions, imported by name. That is the next section, and it is the right home for most of what lives in a Module today.Modules & Async
Imports becomes import
A file is a module, and there is no namespace declaration at all — the file path is the name.
Option Strict On
Imports System
Imports System.Collections.Generic
Namespace Geometry
Public Module Area
Public Function Rectangle(width As Double, height As Double) As Double
Return width * height
End Function
End Module
End Namespace
Module ImportDemo
Sub Main()
Console.WriteLine(Geometry.Area.Rectangle(3, 4))
End Sub
End Module// area.ts
export function rectangle(width: number, height: number): number {
return width * height;
}
export const UNITS = "cm";
// main.ts
import { rectangle, UNITS } from "./area.js";
import * as area from "./area.js";
console.log(rectangle(3, 4), UNITS);
console.log(area.rectangle(2, 5));Anything marked
export is visible to other files; everything else is private to the file, which replaces both Public/Private at namespace level and the Namespace block itself. Imports Geometry becomes import { rectangle } from "./area.js", naming exactly what you want, or import * as area for the whole module. There are no project-level imports: every file states what it needs. Note the .js extension in the path even though the file is .ts — that is the ESM rule, and it catches everyone once.Task becomes Promise
The object representing "a value that is not here yet" has a different name and one important difference in when it starts.
Option Strict On
Imports System
Imports System.Threading.Tasks
Module TaskDemo
Async Function FetchAsync(label As String) As Task(Of String)
Await Task.Delay(10)
Return $"result for {label}"
End Function
Sub Main()
Dim value As String = FetchAsync("one").GetAwaiter().GetResult()
Console.WriteLine(value)
End Sub
End Modulefunction delay(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function fetchValue(label: string): Promise<string> {
return delay(10).then(() => `result for ${label}`);
}
fetchValue("one").then((value) => console.log(value));Task(Of String) becomes Promise<string> and Task becomes Promise<void>. .then(callback) is what ContinueWith was. The behavioral difference: a Promise is always already running — creating one starts the work, and there is no cold task and no Start(). There is also no cancellation token; the closest equivalent is AbortController, and it must be supported by whatever you are calling.Async and Await
The keywords are the same words in lower case, and the top-level bridge the anchor column needs disappears.
Option Strict On
Imports System
Imports System.Threading.Tasks
Module AsyncDemo
Async Function FetchAsync(label As String) As Task(Of String)
Await Task.Delay(10)
Return $"result for {label}"
End Function
Async Function RunAsync() As Task
Dim both = Await Task.WhenAll(FetchAsync("one"), FetchAsync("two"))
For Each value As String In both
Console.WriteLine(value)
Next
End Function
Sub Main()
RunAsync().GetAwaiter().GetResult()
End Sub
End Modulefunction delay(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
async function fetchValue(label: string): Promise<string> {
await delay(10);
return `result for ${label}`;
}
const both = await Promise.all([fetchValue("one"), fetchValue("two")]);
for (const value of both) {
console.log(value);
}Async Function ... As Task(Of T) becomes async function ... : Promise<T>, and Await becomes await. Task.WhenAll becomes Promise.all; Task.WhenAny becomes Promise.race. await works at the top level of a module, so no GetAwaiter().GetResult() bridge is needed. The single biggest difference underneath: JavaScript is single-threaded. await yields to the event loop rather than to another thread, so there is no thread pool, no deadlock from blocking on a task, and nothing like ConfigureAwait — but also no parallelism without spawning a worker.Errors inside async code
A rejected promise reaches a
catch only if you await it — and forgetting to is the most common async bug in the language.Option Strict On
Imports System
Imports System.Threading.Tasks
Module AsyncErrorDemo
Async Function FailAsync() As Task(Of Integer)
Await Task.Delay(1)
Throw New InvalidOperationException("it broke")
End Function
Async Function RunAsync() As Task
Try
Await FailAsync()
Catch error_ As InvalidOperationException
Console.WriteLine($"caught: {error_.Message}")
End Try
End Function
Sub Main()
RunAsync().GetAwaiter().GetResult()
End Sub
End Moduleasync function fail(): Promise<number> {
throw new Error("it broke");
}
try {
await fail();
} catch (error) {
console.log(`caught: ${(error as Error).message}`);
}
// Without await, the rejection is NOT caught here
try {
fail().catch((error: Error) => console.log(`caught late: ${error.message}`));
} catch {
console.log("never runs");
}An
async function that throws returns a rejected promise, and await is what turns that back into a thrown exception your try can see. Call it without await and the surrounding try catches nothing, exactly as an un-awaited Task swallows its exception in .NET — except that here it becomes an unhandled rejection that can take the process down. Note also the cast in the catch: TypeScript types the caught value as unknown, because JavaScript permits throwing anything at all.Error Handling
Try/Catch, with only one catch
The construct is the same shape with one branch instead of several, and the branching moves inside.
Option Strict On
Imports System
Module TryDemo
Sub Main()
Try
Dim numbers() As Integer = {1, 2, 3}
Console.WriteLine(numbers(10))
Catch error_ As IndexOutOfRangeException
Console.WriteLine($"Out of range: {error_.Message}")
Catch error_ As Exception
Console.WriteLine($"Something else: {error_.Message}")
Finally
Console.WriteLine("always runs")
End Try
End Sub
End Moduletry {
const value: unknown = JSON.parse("{ not json }");
console.log(value);
} catch (error) {
if (error instanceof SyntaxError) {
console.log(`Bad JSON: ${error.message}`);
} else if (error instanceof Error) {
console.log(`Something else: ${error.message}`);
}
} finally {
console.log("always runs");
}There is one
catch block and it takes no type — so several typed Catch clauses become a chain of instanceof tests inside a single handler. There is no exception filter, so When becomes an if and a rethrow. Two further differences: the caught value is typed unknown, because any value may be thrown, not just an Error; and reading an array past its end returns undefined rather than throwing, which is why this example has to reach for JSON.parse to produce an error at all.Throwing your own error
Defining an error is the inheritance you already saw, plus one line everybody forgets.
Option Strict On
Imports System
Public Class InsufficientFundsException
Inherits Exception
Public ReadOnly Property Shortfall As Decimal
Public Sub New(shortfall As Decimal)
MyBase.New($"Short by {shortfall}")
Me.Shortfall = shortfall
End Sub
End Class
Module ThrowDemo
Sub Withdraw(balance As Decimal, amount As Decimal)
If amount > balance Then
Throw New InsufficientFundsException(amount - balance)
End If
End Sub
Sub Main()
Try
Withdraw(50D, 75D)
Catch error_ As InsufficientFundsException
Console.WriteLine($"{error_.Message} (short {error_.Shortfall})")
End Try
End Sub
End Moduleclass InsufficientFundsError extends Error {
constructor(readonly shortfall: number) {
super(`Short by ${shortfall}`);
this.name = "InsufficientFundsError";
}
}
function withdraw(balance: number, amount: number): void {
if (amount > balance) {
throw new InsufficientFundsError(amount - balance);
}
}
try {
withdraw(50, 75);
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.log(`${error.message} (short ${error.shortfall})`);
}
}Inherits Exception becomes extends Error, Throw New becomes throw new, and MyBase.New(message) becomes super(message). The line with no .NET counterpart is this.name = "...": without it the error prints as a plain Error in logs and stack traces, because name is a normal property rather than something derived from the class. Convention ends the class name in Error, not Exception.There is no Using
The guarantee has to be built out of
try/finally, or borrowed from a helper.Option Strict On
Imports System
Imports System.IO
Module UsingDemo
Sub Main()
Dim contents As String = ""
Using writer As New StringWriter()
writer.WriteLine("first line")
contents = writer.ToString()
End Using
Console.WriteLine(contents.Trim())
End Sub
End Modulefunction withResource<T>(name: string, work: () => T): T {
console.log(`open ${name}`);
try {
return work();
} finally {
console.log(`close ${name}`);
}
}
const contents = withResource("writer", () => "first line");
console.log(contents);There is no
Using and no IDisposable. Deterministic cleanup is written as try { ... } finally { ... }, and the common pattern is to wrap that in a helper that takes a callback, as above — the caller cannot forget the cleanup because the helper owns it. Most Node APIs are async and handle their own cleanup, so this comes up less than you would expect. A newer language feature, using declarations with Symbol.dispose, brings the real construct back, but runtime support is still uneven.⚠ Gotchas for Visual Basic Programmers
⚠ The types are gone at runtime
The most important thing to understand about TypeScript, and the reason it is not the same kind of safety as
Option Strict On.Option Strict On
Imports System
Imports System.Collections.Generic
Module ReflectionDemo
Sub Main()
Dim numbers As New List(Of Integer) From {1, 2, 3}
' The type is real at runtime and can be asked about
Console.WriteLine(numbers.GetType().Name)
Console.WriteLine(numbers.GetType().GetGenericArguments()(0).Name)
Console.WriteLine(TypeOf CObj(numbers) Is List(Of Integer))
End Sub
End Moduleinterface Person {
name: string;
}
const fromTheWire: unknown = JSON.parse('{"name": 42}');
// The compiler is satisfied. The data is wrong.
const person = fromTheWire as Person;
console.log(typeof person.name); // "number", not "string"
// Nothing at runtime knows Person ever existed
console.log(typeof person);TypeScript is a compile-time-only type system. Every annotation, interface and generic is erased, and what runs is plain JavaScript that checks nothing. So a type assertion (
as Person) is a promise to the compiler, not a conversion — nothing verifies it, and bad data walks straight in. There is no reflection, no GetType(), no way to ask an object what interface it satisfies. The consequence for real code is that every value entering your program from outside must be validated at runtime, with hand-written checks or a library such as Zod or Valibot. Trusting as at the boundary is the single most common source of production bugs in TypeScript.⚠ Sort compares numbers as text
Sorting an array of numbers without a comparator gives the wrong answer, silently, and TypeScript will not warn you.
Option Strict On
Imports System
Imports System.Collections.Generic
Module SortDemo
Sub Main()
Dim numbers As New List(Of Integer) From {10, 9, 100, 1}
numbers.Sort()
Console.WriteLine(String.Join(", ", numbers))
End Sub
End Moduleconst numbers = [10, 9, 100, 1];
console.log([...numbers].sort()); // 1, 10, 100, 9
console.log([...numbers].sort((a, b) => a - b)); // 1, 9, 10, 100
console.log(numbers); // sort mutates — copy firstArray.prototype.sort converts every element to a string and sorts lexicographically unless given a comparator — so 9 sorts after 100. Always pass one: (a, b) => a - b for numbers ascending, (a, b) => a.localeCompare(b) for text. The second trap in the same method is that it sorts in place and returns the same array, unlike LINQ's OrderBy — hence the [...numbers] copies above. toSorted() is the newer non-mutating version.⚠ this depends on how the function was called
A method pulled off its object forgets which object it belonged to — which
AddressOf never lets happen.Option Strict On
Imports System
Public Class Counter
Private _total As Integer = 0
Public Sub Increment()
_total += 1
End Sub
Public ReadOnly Property Total As Integer
Get
Return _total
End Get
End Property
End Class
Module ThisDemo
Sub Main()
Dim counter As New Counter()
Dim action As Action = AddressOf counter.Increment
' Me is bound when the delegate is created
action()
action()
Console.WriteLine(counter.Total)
End Sub
End Moduleclass Counter {
#total = 0;
increment(): void {
this.#total += 1;
}
// An arrow property captures its own this at construction
incrementSafely = (): void => {
this.#total += 1;
};
get total(): number {
return this.#total;
}
}
const counter = new Counter();
const loose = counter.increment;
try {
loose(); // this is undefined here
} catch {
console.log("detached method lost its this");
}
const bound = counter.increment.bind(counter);
bound();
counter.incrementSafely();
console.log(counter.total);this is decided by how a function is called, not where it was defined. counter.increment() sets it; const loose = counter.increment; loose() does not, and in a class body (which is strict mode) this is then undefined. This bites constantly when passing a method as a callback. Three fixes: .bind(counter), wrapping in an arrow (() => counter.increment()), or declaring the member as an arrow-function property, which captures this once at construction. Arrow functions have no this of their own — that is the whole reason they are preferred for callbacks.⚠ There is no Decimal, so money needs care
The row a Visual Basic programmer writing an invoicing screen most needs to read before shipping.
Option Strict On
Imports System
Module MoneyDemo
Sub Main()
Dim price As Decimal = 0.1D
Dim tax As Decimal = 0.2D
Console.WriteLine(price + tax)
Console.WriteLine((price + tax) = 0.3D)
End Sub
End Moduleconst price = 0.1;
const tax = 0.2;
console.log(price + tax); // 0.30000000000000004
console.log(price + tax === 0.3); // false
// Store whole cents instead
const priceCents = 10;
const taxCents = 20;
console.log((priceCents + taxCents) / 100);Every
number is a binary float, so 0.1 + 0.2 is not 0.3 — and unlike .NET there is no Decimal to switch to. The two workable answers are storing amounts as integer minor units (whole cents) and dividing only for display, or using a library such as decimal.js or dinero.js. toFixed(2) formats but does not fix the arithmetic. Anywhere a Visual Basic codebase uses Decimal, the port needs a deliberate decision rather than a straight translation.⚠ Two identical objects are not equal
There is no value type, so every object comparison is a reference comparison.
Option Strict On
Imports System
Public Structure Point
Public ReadOnly X As Integer
Public ReadOnly Y As Integer
Public Sub New(x As Integer, y As Integer)
Me.X = x
Me.Y = y
End Sub
End Structure
Module EqualityDemo
Sub Main()
Dim left As New Point(1, 2)
Dim right As New Point(1, 2)
' A Structure compares by value
Console.WriteLine(left.Equals(right))
End Sub
End Moduleconst left = { x: 1, y: 2 };
const right = { x: 1, y: 2 };
console.log(left === right); // false
console.log(left === left); // true
console.log(JSON.stringify(left) === JSON.stringify(right)); // true, but fragile
console.log([1, 2] === [1, 2]); // false=== on objects and arrays compares identity, never contents — so two separately built objects with the same fields are not equal, and neither are two identical arrays. There is no Structure, no record, and no Equals to override that === would consult. Comparing by value means writing it: field by field, or with a library's deep-equal, or by comparing a canonical string. The JSON.stringify trick above works only when key order matches and the values are JSON-safe, so treat it as a debugging aid rather than a technique.⚠ No My namespace, and a very different runtime
Everything the
My namespace gave you has an equivalent — but the bigger change is where the code runs.Option Strict On
Imports System
Module PlatformDemo
Sub Main()
' My.Computer, My.Application, MsgBox, InputBox and the
' whole WinForms designer are Visual Basic conveniences
Console.WriteLine(Environment.MachineName.Length > 0)
Console.WriteLine(IsNumeric("42"))
Console.WriteLine(Now.Year > 2000)
End Sub
End Moduleimport os from "node:os";
console.log(os.hostname().length > 0);
console.log(!Number.isNaN(Number("42")));
console.log(new Date().getFullYear() > 2000);My.Computer.Name is os.hostname(), My.Computer.FileSystem is node:fs, IsNumeric is a Number() plus Number.isNaN, Now is new Date(). The deeper adjustment is that TypeScript runs in two different worlds: Node.js on a server, where node:fs and node:os exist, and the browser, where they do not and document does. MsgBox and InputBox have no counterpart in either — a browser has alert and prompt, which real applications never use. A WinForms screen becomes HTML and CSS, which is the actual work of the port and the part no cheatsheet can shorten.