TypeScript Basics
Type System
TypeScript’s type system is unlike most other strictly typed languages you have probably dealt with because of two features:
- Structural instead of nominal
- Type narrowing
There’s a type hierarchy that span from the infinitely wide “top type” any
(or unknown
) to the infinitely narrow “bottom type” never
. In between, types can be declared as top-level types like number
and string
, or collection types as follows:
let arr: number[];
let mapping: { [field: string]: number };
let mapping2: { specificField: number; anotherField: string };
However typing can also get very specific:
const literal = "type literal"; // Type is actually "type literal."
function stringToNumber(value: "one" | "two"): number {
let output: number;
if (value === "one") {
output = 1;
} else {
// `value` is guaranteed to be "two"
output = 2;
}
return output;
}
You cannot initialize an empty array without specifying a type.
You can define “tuples” as fixed length arrays with mixed types like let tup: [number, string, boolean] = [2, "one", false]
; Labeled tuple types allow you to formalize tuples and provide meaningful labels with autocompleting like so:
type PhoneNumber = [
countryCode: number,
areaCode: number,
number: number
];
Types can be optional using a ?
: { optField?: string }
.
Intersections and Unions
interface TypeA {
sharedField: string
aThing: number
}
interface TypeB {
sharedField: string
bThing: number
}
let x: TypeA | TypeB;
In the above example, the compiler can assume that x
either conforms to TypeA
or TypeB
, but because of this, it will not let you use .aThing
or .bThing
because there is no guarantee. There are ways to perform dynamic type checks, type narrowing, or casting to allow you to use them.
let y: TypeA & TypeB = {
sharedField: "shared",
aThing: 1,
bThing: -9999,
};
Built-in Types
Partial<T>
Pick<T, "prop1" | "prop2" | "etc...">
Extract<>
Exclude<>
Functions
Function signatures can be separated from implementations to support “overloading” and provide specific combinations of variable types. In a way this is kind of like pattern matching in other languages.
// Signatures
function addUp(type: "number", ...rest: number[]): number;
function addUp(type: "string", ...rest: string[]): string;
// Implementation
function addUp(type: "number" | "string", ...rest: (number | string)[]): number | string {
...
}
You can also declare type on this
in the function signature for lexical scope.
User-defined Type Guards
Sometimes the compiler won’t be able to determine that it can safely narrow a type inference, for example, in a conditional block where you’ve explicitly tested the value. In these cases, we can write function signature that hints to the compiler that it should trust that we’ve performed the check manually.
function isDefined<T>(arg: T | undefined): arg is T {
return typeof arg !== 'undefined';
}
Types vs. Interfaces
Type aliases a generally more flexible than interfaces as they can deal with scalar and union types like type Zip = string | number;
, however they cannot be self-referential until TypeScript 4.0+.
Interfaces deal exclusively with entities that extend from the JavaScript Object
type, so functions, arrays, and objects are supported. They can extend
from one another. Types can also handle these things. Interfaces are calculated lazily, while types are calculated
interface Dict {
[key: string]: undefined | {
title: string;
body: string;
};
}
// Interfaces can be combined.
interface Dict {
global: {
title: string;
body: string;
};
}
let x: Dict;
x.global; // Must exist!
x.other // Optionally exists...
Classes
- Fields
- Implements interfaces
- Access modifier keywords
readonly
fields- Definite assignment operator
- Getters and lazily initialized values
Type Constraints
Type constraints allow for genericized type parameters that meet certain requirements necessary for the compiler. This is especially useful for differing objects or dictionaries that must have a certain field. This is a bit different from dictionary type matching in that generics still return the type as-is.
Returns for superclasses and base interfaces reduce the fidelity of the known type, which is when type parameters come in handy.
function test<T extends { field: string }>(list: T[]): T {
...
}
Other Topics
- Construct signatures
- Branded types
- Mapped types and
keyof
- Conditional types and
infer
- Namespacing instead of static properties on classes and functions
class
/interface
/namespace
stacking- ECMAScript private properties with
#
- Nullish coalescing with
??
- Variadic tuple types
- Template type literals