TypeScript Summary
Summary of Execute Program's TypeScript courses and other material.
Intro
TypeScript is a statically typed programming language built on JavaScript. Every TypeScript program is a valid JavaScript program, but with extra type annotations that are erased at compile time and don't exist at runtime.
Why TypeScript
It provides type safety by catching type errors at compile time, it's also useful to define contracts between functions and classes.
Limitations
- It doesn't catch runtime errors. For user input, or data from external services, we need to use schema validation or alternatives like
zod,io-ts, JSON schema, etc. - It's not type sound: static type guarantees are not always kept true at runtime.
Setup
Installation
- Install TypeScript with a Node.js package manager (e.g.
npm) - Compile js files with
npx tsc file.ts - Start the REPL with
tsxorts-node - Use a linter
typescript-eslint
Compiler options
Compiler options can be passed to tsc or moved to a tsconfig.json file.
--strict: enable all strict type checking options--strictNullChecks--noImplicitReturns: complain when a function declares a return type but some code paths don't return a value--noFallThroughCasesInSwitch--noUnusedLocals
The strict mode enables many strict options, listed below:
/* tsconfig.json. */
{
"compilerOptions": {
"strict": true,
},
}
The noImplicitAny flag disallows function parameters with no type.
function add(x, y) {
return x + y;
}
=> type error: Parameter 'x' implicitly has an 'any' type.
The strictNullChecks compiler flag doesn't allow us to assign null or undefined to variables that don't include those types.
const n: number = null;
=> type error: Type 'null' is not assignable to type 'number'.
The strictPropertyInitialization compiler flag forces us to assign class properties in the constructor. Otherwise, they could end up undefined, which would violate their type.
class User {
name: string;
age: number;
constructor(name: string) {
this.name = name;
}
}
const alex: User = new User('alex');
=> type error: Property 'age' has no initializer and is not definitely assigned in the constructor.
REPL
Installed from ts-node, by https://github.com/TypeStrong/ts-node.
Import modules inside the REPL:
const util = await import('util')
const D = await import('io-ts/Decoder')
Inspect nested objects:
const util = await import('util')
util.inspect(test, { depth: null })
Data Types
Basic types: number, boolean, string
Tuples are arrays of fixed length, [number, number]
Literal type: use literal values as types. type sort = 'asc' | 'desc'
any: allows any value, effectively disables the type system for that expression, avoid!
const n: any = 5;
/* The static type of `s` is `string`, but it contains a `number`! */
const s: string = n;
s;
=> 5
unknown: a safe version of any. TypeScript won't let us use an unknown unless we narrow it to a more specific type.
const n: unknown = 5;
const n2: number = n;
=> type error: Type 'unknown' is not assignable to type 'number'.
object: allows objects with any properties, but we can't access those properties. Found mostly in legacy code.
const user: object = {
name: 'Alex',
};
user.name;
=> type error: Property 'name' does not exist on type 'object'.
void: Functions without return values have void as their return type.
function f() { }
const n: number = f();
=> type error: Type 'void' is not assignable to type 'number'.
never: used for values that will never actually occur at runtime.
function throws(): never {
throw new Error('oh no');
}
throws();
=> Error: oh no
Set, Map, Date, Regexp, Error, Array, Promise.
const s: Set<number> = new Set([1, 2, 3]);
s.add('this is not a number');
=> type error: Argument of type 'string' is not assignable to parameter of type 'number'.
Promises or Async/Await
const promise: Promise<number> = Promise.resolve(100);
promise;
=> {fulfilled: 100}
async function asyncDouble(
x: Promise<number>
): Promise<number> {
return 2 * await x;
}
asyncDouble(Promise.resolve(5));
=> {fulfilled: 10}
any or unknown (recommended).
let error: Error;
try {
throw new Error('something broke');
} catch (e: unknown) {
if (e instanceof Error) {
error = e;
} else {
throw new Error("We can't catch that!");
}
}
error.message;
=> 'something broke'
Promise.reject('it failed')
.catch((reason: unknown) => {
if (typeof reason === 'string') {
return reason
} else {
return reason.toString()
}
});
=> {fulfilled: 'it failed'}
Classes
TypeScript supports static typing of JavaScript classes.
class Cat {
name: string;
#vaccinated: boolean;
constructor(
name: string,
vaccinated: boolean
) {
this.name = name;
this.#vaccinated = vaccinated;
}
needsVaccination(): boolean {
return !this.#vaccinated;
}
}
type HasName = {
name: string
};
class User implements HasName {
name: string;
constructor(name: string) {
this.name = name;
}
}
const alex = new User('Alex');
const hasName: HasName = alex;
hasName.name;
=> 'Alex'
Functions
Declare function explicitly:
// traditional functions
function myFunction(x: string): string {
return x;
}
// arrow functions
const myFunction2: (x: string) => string = (x) => {
return x;
}
function callFunction(f: () => number) {
return f();
}
callFunction(() => 1);
=> 1
Function parameters can be optional.
function add(x: number, y?: number) {
return x + (y ?? 1);
}
[add(3, 4), add(3)];
=> [7, 4]
Parameters can have default values.
function add(x: number, y: number = 1) {
return x + y;
}
[add(10, 20), add(30)];
=> [30, 31]
Functions can have "rest parameters", which gather an arbitrary number of arguments into a single local variable.
function add(...numbers: number[]) {
let sum = 0;
for (const n of numbers) {
sum += n;
}
return sum;
}
[add(), add(1, 2), add(100, 200, 300)];
=> [0, 3, 600]
Hybrid types let us define function types that also have properties like an object does.
/* This is a function type, but it also has a
* 'prefix' property. */
interface Path {
(suffix: string): string
prefix: string
}
Function overloads let us write multiple type signatures for a single function. The main function body must have a type that's simultaneously compatible with all of the signatures.
When TypeScript type checks the function body, it only checks it against the main type, not the individual overload types. Mistakes in the function body can allow type errors to pass silently.
/* This function returns a number when given a
* number, and returns a string when given a
* string. */
function double(n: number): number;
function double(s: string): string;
function double(
n: number | string
): number | string {
/* implementation goes here */
}
When considering function parameters, type compatibility is "backwards".
function takesLiteral(s: 'age'): string {
return s;
}
type TakesString = (s: string) => string;
/* This is a type error. We can't assign a
* function that takes the literal string 'age'
* to a function type that takes a `string`. */
const testFunction: TakesString = takesLiteral;
When considering function return types, type compatibility works normally.
function returnsString(): string {
return 'age';
}
type ReturnsLiteral = () => 'age';
/* This is a type error due to the function
* return type compatibility rules. If
* TypeScript allowed this, `testFunction`'s
* type would say "I return 'age'", but the
* underlying function's type can return any
* string. */
const testFunc: ReturnsLiteral = returnsString;
We can use function overloads to statically type complex functions with option parameters.
/* This function returns different static types
* depending on the options we pass in. We
* define two signatures, then the body. */
function findUser(id: number): User;
function findUser(
id: number,
options: {withComments: true}
): UserWithComments;
function findUser(
id: number,
options: {withComments?: boolean} = {}
): User | UserWithComments {
/* implementation goes here */
}
Objects
TypeScript's structural type system allows extra properties on objects. At runtime, our objects may have properties that aren't specified in their static types.
const user = {
name: 'Alex',
email: 'alex@example.com'
};
const hasEmail: {email: string} = user;
/* `hasEmail`'s static type doesn't have a
* `name` property, but the property is still
* present at runtime. */
(hasEmail as any).name;
=> 'Alex'
The empty object type {} allows any object. It specifies no properties, so we can't access any properties on the object.
function takesAnObject(obj: {}) {
return 'it worked';
}
[
takesAnObject({name: 'Alex'}),
takesAnObject({loginCount: 55}),
];
=> ['it worked', 'it worked']
Object types can contain method definitions in addition to regular properties.
type Dog = {
name: string
bark(): string
};
const dog: Dog = {
name: 'Luna',
bark: () => 'woof',
};
dog.bark();
=> 'woof'
The object spread operator works in TypeScript, and it enforces types as usual.
const partialAlex = {name: 'Alex'};
const fullAlex = {...partialAlex, admin: true};
fullAlex;
=> {name: 'Alex', admin: true}
If we specify unknown properties when defining an object literal, TypeScript gives us a type error. This stops us from specifying useless properties, and it can even prevent bugs in some cases.
const user: {name: string} = {
name: 'Alex',
age: 36
};
=> type error: Type '{ name: string; age: number; }' is not assignable to type '{ name: string; }'.
Object literal may only specify known properties, and 'age' does not exist in type '{ name: string; }'.
We can statically type complex functions with option types by combining default parameter values and optional object properties.
/* Callers can specify none, some, or all of
* `opts`. We'll use default values for any
* options that are missing. */
function sendEmail(
to: string,
opts: {
tracking?: boolean
retries?: number
} = {}
) {
const tracking = opts.tracking ?? true;
const retries = opts.retries ?? 3;
/* Send email here. */
}
Arrays
Arrays are technically objects, so typeof returns 'object'. This behavior originally comes from JavaScript.
typeof [1, 2, 3];
=> 'object'
typeof someArray returns 'object'. To narrow an array, we have to use Array.isArray instead.
function maybeLength(maybeArray: unknown) {
if (Array.isArray(maybeArray)) {
return maybeArray.length;
} else {
return undefined;
}
}
Operators and Keywords
type and interface define custom types.
Type algebra: TypeScript types follow algebraic laws, including the distributive law.
// These two types are equivalent.
type T1 = IsAdmin & (User | Cat);
type T2 = (IsAdmin & User) | (IsAdmin & Cat);
is is a type predicate operator for custom type guards.
function isFoo(input: any): input is Foo {
return input.foo !== undefined
}
if (isFoo(x)) ... // type guard
as operator forces a value to have a certain static type. It's very unsafe, like any, because it lets us specify types that are incorrect.
const obj = {};
/* This is supposed to be a `string`, but it's
* an object. */
/* The static type here is `string`, but our
* variable actually holds `object`. */
const wronglyTypedObj: string = obj as string;
keyof operator returns an object type's keys as a union of literal string types.
type User = {
name: string
age: number
};
// This is a 'name' | 'age'.
type UserKey = keyof User;
typeof operator returns the static type of a variable, function, class, etc.
const one: number = 1;
// This type is `number`.
const two: typeof one = 2;
infer keyword can be used inside of conditional types to extract a type parameter from a generic type.
/* This type extracts the type inside of an
* array type. This would be impossible without
* `infer`. */
type ArrayContents<T> =
T extends Array<infer ElementType>
? ElementType
: never;
// This type is `number`.
type MyNumber = ArrayContents<Array<number>>;
?. operator is like the regular . operator for property access. However, if the value to the left of the ?. is null or undefined, then the overall expression returns undefined.
function getUser(): User | undefined {
return {name: 'Alex'};
}
/* Return the user's name, or undefined if we
* got undefined back. */
getUser()?.name;
?? is similar to ||, but more safe. It returns its right side when its left side is either undefined or null. It's most often used to specify default values for when we encounter an undefined or null.
> null ?? 1;
=> 1
> undefined ?? 'fallback';
=> 'fallback'
> false ?? [2];
=> false
The satisfies operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression.
interface Status {
status: "failure" | "success";
}
const info: Status = {
status: "failure",
};
info.status; // this type is: "failure" | "success"
const info2 = {
status: "failure",
} satisfies Status;
info2.status; // this type is "failure"
as const creates immutable types and narrows the type of a variable to its literal value, making object properties readonly and turning arrays into readonly tuples.
// this type is: readonly ['admin', 'user']
const roles = ['admin', 'user'] as const;
// this type is "admin" | "user"
type Role = typeof roles[number];
Template literal types allow us to create dynamic types using string literals.
type UserId = `user_${number}`;
const user: UserId = 'user_123';
const notUser: UserId = 'not_user_123';
=> Type '"not_user_123"' is not assignable to type '`user_${number}`'.
Defining and constructing types
Type and Interface
Declare object types with interface or type.
// User1 and User2 are equivalent
interface User1 {
name: string
}
type User2 = {
name: string
};
interface can only describe object shapes, and can be extended.
Intersection
A & B means "any type that's compatible with both A and B.
type HasEmail = {email: string};
type CanBeAdmin = {admin: boolean};
// This type is {email: string, admin: boolean}.
type User = HasEmail & CanBeAdmin;
We can use intersections to share fields between object types.
type HasEmail = {email: string};
type User = HasEmail & {
admin: boolean
};
type Company = HasEmail & {
accountNumber: number
};
never.
type HasEmail1 = {
email: string
};
type HasEmail2 = {
email: number
};
/* The `email` property can't be both a string and a number. */
type User = HasEmail1 & HasEmail2;
const user: User = {
email: 'alex@example.com'
};
=> type error: Type 'string' is not assignable to type 'never'.
Union
A | B means "any type that's compatible with either A or B.
type accountId = string | number;
Discriminated unions are unions of multiple object types. They share a property called the discriminator, where each object type specifies a different literal value for the discriminator. At runtime, we can use the discriminator property to decide which union alternative we're looking at. Discriminators can be any literal type.
type StartedCourse = {
started: true // discriminator
lastInteractionTime: Date
};
type UnstartedCourse = {
started: false // discriminator
};
type Course = StartedCourse | UnstartedCourse;
Discriminated unions are also useful for handling errors. The "success" side of the union contains the result value, and the "failure" side contains details about what went wrong.
type ConversionSucceeded = {
kind: 'success'
value: number
};
type ConversionFailed = {
kind: 'failure'
reason: string
};
type Conversion =
ConversionSucceeded | ConversionFailed;
function safeNumber(s: string): Conversion {
/* convert to number here */
}
const maybeNumber = safeNumber(s)
if (maybeNumber.kind === 'success') {
// access value
} else {
// access reason
}
never has no effect. T | never is always just T.
function returnNumberOrNever(): number | never {
return 1;
}
// The return type of the function is `number`.
const n: number = returnNumberOrNever();
When indexing into an object type with a union, the indexing operator "distributes over" the union.
type User = {
name: string
age: number
};
// This is a `string | number`.
type PropertyTypes = User['name' | 'age'];
Optional properties
Object properties can be optional by adding the ? syntax. We don't have to specify optional properties when building an object at runtime.
type User = {
name: string
postalCode?: string
};
const alex: User = {
name: 'Alex',
};
alex.postalCode;
=> undefined
Extending types
Interfaces can extend other interfaces as well as classes.
interface CanBeAdmin {
admin: boolean
}
interface User extends CanBeAdmin {
name: string
}
const user: User = {name: 'Alex', admin: true};
user;
=> {name: 'Alex', admin: true}
Index Signatures
Index signatures allow us to access any property name on an object. We don't have to declare the properties one-by-one. All of the properties must have the same static type.
type LoginCounts = {
// This is the index signature.
[userName: string]: number
};
const loginCounts: LoginCounts = {
Alex: 5,
Bernd: 7,
};
TypeScript can't know whether any given property name will actually exist or not. If we access a property that doesn't exist, we'll get undefined, which violates our declared static type.
type UserEmails = {[name: string]: string};
const userEmails: UserEmails = {
Alex: 'alex@example.com'
};
/* The type of this expression is `string`, but
* we actually get `undefined` at runtime. */
userEmails.Bernd;
=> undefined
Indexing into object and array types
We can index into object types directly, giving us the types of individual properties.
type Album = {
name: string
copiesSold: number
};
// This type is `string`.
type AlbumName = Album['name'];
Indexing into an array type gives us the array's element type, no matter which index we ask for.
type Names = string[];
// This type is `string`.
const username: Names[0] = 'alex';
type StringAndNumber = [string, number];
// This type is `number`.
const age: StringAndNumber[1] = 29;
Recursive types
Types can refer to themselves.
> type Nested = number | Nested[];
const n: Nested = [[[[1]], 2, [3]]];
Mapped Types
Mapped types turn one object type into another. They "map over" the property types, replacing them with new types.
type User = {
email: string
};
// This type is {email: Array<string>}.
type UserAggregate = {
// This is the mapped type.
[K in keyof User]: Array<User[K]>
};
Partial type is implemented as a mapped type.
type OurPartial<T> = {
[P in keyof T]?: T[P];
};
type Nullable<T> = {
// This is the mapped type.
[K in keyof T]: T[K] | null
};
type LoginCounts = {
// This is the index signature.
[user: string]: number
};
// This is {[user: string]: number | null}.
type NullableLoginCount = Nullable<LoginCounts>;
keyof with typeof
keyof and typeof are often used together. We can use them to get a type for the object keys of an existing variable.
const icons = {
rightArrow: 'right arrow image',
billing: 'billing image',
};
// This is 'rightArrow' | 'billing'.
type IconName = keyof typeof icons;
Conditional Types
Conditional types have one of two types, depending on whether a certain condition is true. TypeScript only supports conditions that use extends.
type WrapStringInArray<T> =
T extends string ? Array<string> : T;
// The type here is `Array<string>`.
const s: WrapStringInArray<string> = ['hello'];
type Exclude<Original, ToExclude> =
Original extends ToExclude ? never : Original;
// This type is `C | D`.
type Excluded = Exclude<A|B|C|D, A|B>;
Type Predicates
Type predicates in TypeScript are functions that help narrow down types by asserting that a variable is of a specific type if the function returns true.
Type predicates allow us to write our own type guard functions. They have someArgument is SomeType in place of a regular return value.
> const maybeAddress: Address | undefined = getAddress();
function isAddress(
address: Address | undefined
): address is Address {
return address !== undefined;
}
if (isAddress(maybeAddress)) {
/* In here, the type of `address` is
* `Address`. */
}
/* Down here, the type of `address` is
*`Address | undefined` again. */
The array filter function often benefits from type predicates.
Normally, filter returns an array of the same type, even if we pass a function that seems like it should narrow the type. If we make our function a type predicate, then our filter call can return a narrowed type rather than the original array's type.
function isNumber(
maybeNumber: number | string
): maybeNumber is number {
return typeof maybeNumber === 'number';
}
const numbersAndStrings: (number | string)[] =
[1, 'a', 2, 'b'];
/* This assignment works because `filter`
* respects our `isNumber` type predicate. This
* `filter` call narrows the array from
* (number | string)[] to just number[]. */
const numbers: number[] =
numbersAndStrings.filter(isNumber);
If we make a mistake and implement our type predicate function incorrectly, TypeScript won't know. It will blindly trust our predicate, which can lead to incorrect types.
> function isNumber(
n: number | string
): n is number {
return typeof n === 'string';
}
const n: string = 'oh no';
/* This is a bug! Our `isNumber` function is
* wrong, so it lets us put a string in a
* number variable. */
const n2: number = isNumber(n) ? n : 0;
n2;
=> 'oh no'
Type predicate arguments are a great place to use unknown. Then they can work with any argument.
function isString(
s: unknown
): s is string {
return typeof s === 'string';
}
const s: unknown = {isCat: true};
const s2: string = isString(s)
? s
: 'not a string';
s2;
=> 'not a string'
Assertion functions throw an error unless some condition is true. When we call one, TypeScript knows that the asserted condition must be true after that point.
function assert(cond: boolean): asserts cond {
if (!cond) {
throw new Error('Failed');
}
}
assert(typeof numberOrString === 'number');
/* `numberOrString` was a `number | string`.
* After our assertion, it's narrowed to just
* `number`. */
Enum
Enums allow us to express types that only accept certain literal values. Discouraged.
enum HttpMethod {
Get = 'GET',
Post = 'POST',
}
const method: HttpMethod = HttpMethod.Post;
method;
=> 'POST'
Generics
Generics are data types that take another type as an argument, e.g. Array<string>.
Generic object types, interfaces, and classes all have generic type parameters immediately after the type's name.
type Bag<T> = {
value: T
};
const returnsItsArgument = <T>(arg: T): T => {
return arg;
};
> type OurArray<T=string> = Array<T>;
// This is an Array<string>.
const names: OurArray = ['Alex', 'Bernd'];
Generic Constraints: Constraints force a generic type parameter to extend some other type. This lets us introduce limitations like "this function is generic, but the parameter must be an object with an age parameter".
/* `filterByAge` works with arrays of any
* object type, as long as it has an
* `age: number` property. */
function filterByAge<T extends {age: number}>(
things: Array<T>,
max: number
): Array<T> {
return things.filter(
thing => thing.age < max
);
}
unknown in those cases.
type ItemsObject<A extends Array<unknown>> = {
items: A
};
const someNumbers: ItemsObject<number[]> = {
items: [1, 2, 3]
};
Constraints and Type Compatibility: When we have a type constraint like <T extends string>, it doesn't mean "T is a string". It means "T is some type that's compatible with string". That type could be 'name' or 'name' | 'age' or just string.
type SpecificKey<T extends string> = {key: T};
// This type is {key: 'name' | 'age'}.
type NameOrAgeObj = SpecificKey<'name' | 'age'>;
keyof in generic constraints, like Key extends keyof T. That means "Key is any type that's compatible with one or more of T's keys".
function get<T, Key extends keyof T>(
obj: T,
key: Key
): T[Key] {
return obj[key];
}
/* The `get` function uses a keyof constraint to
* return the correct type for the requested
* property key. This call returns a string, but
* it would return a number if we asked for
* 'age' instead of 'name'. */
const name: string = get(
{name: 'Alex', age: 36},
'name'
);
Utility Types
Record
Record is used to construct object types. It's useful when mapping properties of one type to another.
interface UserInfo {
email: string;
age: number;
}
// Here we map type `string` to type `UserInfo`
const users: Record<string, UserInfo> = {
alex: { age: 30, email: "alex@example.com" },
bard: { age: 40, email: "bard@example.com" }
};
Pick & Omit
With the Pick type we choose which properties to include.
type User = {
name: string
email: string
age: number
};
// This type is {name: string}.
type NameOnly = Pick<User, 'name'>;
Omit type lets us create a new object type with some properties removed.
type User = {
name: string
email: string
age: number
};
// This type is {name: string}.
type NameOnly = Omit<User, 'email' | 'age'>;
Partial & Required
The Partial generic type makes every property in an object type optional.
type User = {
name: string
postalCode: string
};
const partialAlex: Partial<User> = {
postalCode: '7010'
};
partialAlex.postalCode;
=> '7010'
We can use the Partial type to create object template systems.
type User = {
name: string
country: string
};
const template: User = {
name: 'Alex',
country: 'UK',
};
function override(
overrides: Partial<User>
): User {
return {...template, ...overrides};
}
override({name: 'Bernd'});
=> {name: 'Bernd', country: 'UK'}
Readonly
The readonly modifier makes an individual object property read-only.
interface User {
readonly name: string
}
const alex: User = {name: 'Alex'};
alex.name = 'Bernd';
=> type error: Cannot assign to 'name' because it is a read-only property.
Readonly generic type makes all properties on an object type read-only.
type User = Readonly<{
name: string
age: number
}>;
const alex: User = {name: 'Alex', age: 36};
alex.name = 'Bernd';
=> type error: Cannot assign to 'name' because it is a read-only property.
Readonly Properties vs. Values:
type User = {
name: string
readonly catNames: string[]
};
const alex: User = {
name: 'Alex',
catNames: ['Ms. Fluff'],
};
alex.catNames.push('X') // no error
alex.catNames = 'X'
=> type error: Cannot assign to 'catNames' because it is a read-only property.
Read-only arrays act like normal arrays, except that we can't modify their elements. We also can't call any methods that would change the array, like .push(...) or .sort().
const strings: ReadonlyArray<string> = [
'a',
'b',
'c'
];
strings.push('d');
=> type error: Property 'push' does not exist on type 'readonly string[]'.
ReturnType, Parameters & Awaited
The ReturnType generic type gives us a function's return type.
type StringFunction = () => string;
// This type is `string`.
type OurString = ReturnType<StringFunction>;
Parameters generic type gives us a function's parameters as a tuple type.
type DoubleFunction = (n: number) => number;
// This type is `number`.
type OurNumber = Parameters<DoubleFunction>[0];
Awaited generic type recursively unwrap a Promise to reveal the type.
// this type is `string`
type A = Awaited<Promise<string>>;
Other Utility Types
Other useful utility types can be found on the TypeScript official site
Namespaces
Namespaces are like modules, except that more than one namespace can live in a single file. TypeScript supports namespaces, but they're generally discouraged in new code. We recommend using separate modules in their own files instead.
namespace Util {
export function wordCount(s: string) {
return s.split(/\b\w+\b/g).length - 1;
}
}
namespace Tests {
export function testWordCount() {
if (Util.wordCount('hello there') !== 2) {
throw new Error('Word count mismatch');
}
return 'ok';
}
}
Tests.testWordCount();
=> 'ok'
Errors
- Syntax error are caught by the JavaScript runtime
- Type errors caught by the TypeScript compiler:
'2' * '2' => error: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.{ a: 'a' }.b() => error: Property 'b' does not exist on type '{ a: string; }' - Undefined in arrays (not an error by default!)
avoid by enabling two compiler options:
const strings: string[] = ['a', 'b'] const element: string = strings[10] // should be typed as undefined, but is typed as string--strictNullChecksand--noUncheckedIndexedAccess:const strings: string[] = ['a', 'b'] const element: string = strings[10] // should be typed as undefined, but is typed as string => Type 'string | undefined' is not assignable to type 'string'. - Impossible conditions: If condition in an
ifis impossible, the compiler will complain.let s; if (1 === 0) { s = 'it worked'; } => type error: This comparison appears to be unintentional because the types '1' and '0' have no overlap. - No exhaustiveness checking: To make the compiler check a
switchstatement exhaustively, declare an explicit return type for the function, then write aswitchwhere each case returns.type User = {kind: 'user', userName: string}; type Cat = {kind: 'cat', catName: string}; type Dog = {kind: 'dog', dogName: string}; function getName( hasName: User | Cat | Dog, ): string { switch (hasName.kind) { case 'user': return hasName.userName; case 'cat': return hasName.catName; } } => type error: Function lacks ending return statement and return type does not include 'undefined'.
Terms
Type inference
the compiler infers the data type of an expression or a function return type without explicitly declaring it
Type erasure
types are only used for type checking, then erased from the compiled output
Generics
Data type that takes another type as an argument, e.g.
Array<string>
Literal type
use literal values as types.
type sort = 'asc' | 'desc'
Literal object type
Declare the object type directly in the function signature (instead of naming it using
type)function userName(user: { name: string }) { return user.name; }
Object narrowing / type compatibility
Narrow the larger type to a smaller type. TypeScript will automatically do this.
Conditional narrowing: Using a conditional statement & type guards to narrow the type of the variable.type User = { name: string, email: string } // larger type type HasEmail = {email: string } // smaller type function sendEmail(args: HasEmail) { .. } // both work because TypeScript does object narrowing sendEmail(user: User) sendEmail(email: HasEmail)Logical operators likeconst userOrUsers: User | User[] = { name: 'test' } function nameOrLength(arg: User | User[]) { if (Array.isArray(arg)) { return arg.length } else { return arg.name } }&&narrow types. The right side of the&&is only evaluated if the left side is true, so it acts like a conditional.Ternary conditionals also narrow types.function arrayLength( strings: string[] | undefined ): number | undefined { return strings && strings.length; }function lengthOrNumber( n: number | number[] ): number { return Array.isArray(n) ? n.length : n; }
Type guard
Function or expression when used in a conditional statement will guarantee the type of the variable.
Array.isArray()or theiskeyword Guarantees that code executes only for certain types
Structural typing
The object's structure determines the type. AKA duck typing.
Nominal typing
Type compatibility depends on explicit declarations.
User is a bigger kind of HasEmail
Type soundness
In a sound type system, all static type guarantees remain true at runtime. TypeScript is unsound: in some situations, the static types will be violated at runtime.
let names: string[] = ['Alex', 'Bernd']; let unsoundNames: (string | number)[] = names; /* This is unsound! It causes the `names` array * to contain a number, but `names` is supposed * to be a `string[]`! */ unsoundNames.push(5); names; => ['Alex', 'Bernd', 5]
Type widening
TypeScript widens the types of literal values where possible.
Sometimes we don't want TypeScript to widen the types in this way. We can add// This type is {name: string}. let user = {name: 'Alex'};as constto prevent widening of a specific expression.// This type is {name: 'Alex'}. let user = {name: 'Alex' as const};