Typescript terminology

what do the words mean

article image
Javascript developers NOT using Typescript

General TS

typescript is a superset of javascript

the compiler is called tsc

tsc actually transpile -converts code into a subset language-

triple-slash-directives: indicate the js target for a specific file:

/// <reference lib="es2016.array.include" />
[ 'foo', 'bar', 'baz' ].includes('bar'); // true

Type Systems

Invariance:

  • does not accept supertypes.
  • does not accept subtypes.

Covariance

  • does not accept supertypes.
  • does accept subtypes.

Contravariance

  • does accept supertypes.
  • does not accept subtypes.

Bivariance

  • does accept supertypes.
  • does accept subtypes.

TS Specifics

Deep const: Declare a const as really unmutable (not like js const, that will prevent only from changing the reference)

*const assertions can only be applied immediately on simple literal expressions

const myInmutableObject = { name: 'sergio', age: 42 } as const // properties are read-only now (same with arrays, and any other LITERAL)

refer: When ts guess the type for you

type assertion: Explicity indicate the resulting type of an expression:

const num = numberStringSwap('1234') as string

string literal types: we want to indicate a particular string literal (or group of strings) as type for a value.

let easing: "ease-in" | "ease-out" | "ease-in-out"; // a group, in this case

templace literal types: made out of several string literal types:

type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";

function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void

object type: Indicates "non primitive value"

typeof: to reduce type duplication

let point: { x: number; y: number; };
let point2: typeof point;

interfaces: A named object type literal

type aliases: Very similar to interfaces. Unlike interfaces, aliases are not subject to declaration merging.

interface vs. type:

Typescript has both interface and type aliases but they can often be used incorrectly. One of the key differences between the two of these is that an Interface is limited to describing Object structures whereas type can consist of Objects, primitives, union types, etc.

Another difference here is their intended use. An interface primarily describes how something should be implemented and should be used. A type on the other hand is a definition of a type of data.

** declaration merging: Declaring several times the same will result in merging into a single interface.

index signature: enables arbitrary keys to be defined on an object

interface HashMapOfPoints {
[key: string]: Point;
}

// it can use strings, numbers, symbols and template string literals

interface Configuration {
[key: symbol]: string | number;
[key: `service-${string}`]: string | number;
}

//Interface properties can also be named using constant values, similar to computed property names on normal objects. Computed values must be constant strings, numbers, or Symbols:

const Foo = 'Foo';
const Bar = 'Bar';
const Baz = Symbol();

interface MyInterface {
[Foo]: number;
[Bar]: string;
[Baz]?: boolean; // and this property is optional
}

// syntax for methods
interface Point {
x: number;
toGeo: () => Point;
// or
toGeo(): Point;
// or, if it's optional
toGeo?(): Point;
}

tuple types: an array which elements are typed one-by-one:

let point: [ number, number, number ] = [ 0, 0, 0 ];

// supports optional values:
let point: [ number, number, number? ] = [ 0, 0, 0 ];

// supports spread:
let bar: [boolean, ...string[], boolean]; // *
A rest element cannot be followed by another rest element or an optional element.  There can only be one rest element in a tuple type.

function types: defined using arrow syntax

// example:
let printPoint: (point: Point) => string;

// passing functions as arguments
let printPoint: (getPoint: () => Point) => string;

// can also be described using the object literal syntax
let printPoint: { (point: Point): string; };

// Functions that are object constructors:
let Point: { new (): Point; };
// or
let Point: new () => Point;

overloaded functions: Disgusting way of functions with different number and types of parameters.

function numberStringSwap(value: number, radix?: number): string;
function numberStringSwap(value: string): number;
function numberStringSwap(value: any, radix: number = 10): any {
if (typeof value === 'string') {
return parseInt(value, radix);
} else if (typeof value === 'number') {
return String(value);
}
}

// Here, even though the second overload signature is more specific, the first will be used. This means that you
// always need to make sure your source code is ordered so more specific overloads won’t be shadowed by more
// general ones.
function numberStringSwap(value: any): any;
function numberStringSwap(value: number): string;

rest parameters: Some functions may take an unspecified number of parameters. TypeScript allows expressing these using a rest parameter.

interface Array<T> {
push(...args: T[]): number;
}

**
Without the use of rest parameters, you would need to write an overload for every number of arguments that the function needs to accept

generic types: type that must include or reference another type in order to be complete

interface Arrayish<T> {
map<U>(
callback: (value: T, index: number, array: Arrayish<T>) => U,
thisArg?: any
): Array<U>;
}

** The placeholder types inside the angle brackets are called type parameters


!! important !! type parameters can be constrained to a specific type by using the extends keyword within the type parameter.


union types: allow a parameter or variable to support more than one type

declare function byId(element: string | Element): Element

// When declaring a generic type as a union type, a default type can be included

type Data<T extends string | number = number> = { value: T };

type guard: code used to narrow types. We can use typeof, instanceof and in

function byId(element: string | Element): Element {
if (typeof element === 'string') {
return document.getElementById(element);
} else {
return element;
}
}

can also use other stategies to check that it's the right type. It's pretty clever, actually

interface Data {
type: ${string}Result;
data: string;
}
interface Failure {
type: ${string}Error;
message: string;
}
function handleResponse(response: Data | Failure) {
if (response.type === 'DatabaseResult') {
// Response type is narrowed to Data.
handleDatabaseData(response.data);
} else if (response.type === 'DatabaseError') {
// Response type is narrowed to Failure.
handleDatabaseError(response.message);
}
}

intersection types: indicate that a value will be a combination of multiple types

interface Foo {
name: string;
count: number;
}

interface Bar {
name: string;
age: number;
}

export type FooBar = Foo & Bar;

mapped types: Mapped types allow for the creation of new types based on existing types by mapping properties of an existing type to a new type.

type Stringify<T> = {
[P in keyof T]: string;
};

interface Point { x: number; y: number; }
type StringPoint = Stringify<Point>;
const pointA: StringPoint = { x: '4', y: '3' }; // valid

// A mapped type is a generic type which uses a union of PropertyKeys (frequently created via a keyof) to iterate through keys to create a type:

type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};

map type modifiers: ability to add or remove readonly or ? modifiers from mapped properties

type MutableRequired<T> = { -readonly [P in keyof T]-?: T[P] };
type ReadonlyPartial<T> = { +readonly [P in keyof T]+?: T[P] };

map types in tuples:

type ToPromise<T> = { [K in typeof T]: Promise<T[K]> };
type Point = [ number, number ];
type PromisePoint = ToPromise<Point>;
const point: PromisePoint = [ Promise.resolve(2), Promise.resolve(3) ]; // valid

map types using "as": Allows the acceptable keys to be derived from the keys. We can do really cool things here, for example:

interface Person {
name: string;
age: number;
location: string;
}

type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: (arg: T) => T[K]
};

type LazyPerson = Getters<Person>;

const person: Person = {
name: 'sergio',
age: 42,
location: 'Oviedo'
}

const Example: LazyPerson = {
getName: (person: Person) => person.name,
getAge: (person: Person) => person.age,
getLocation: (person: Person) => person.location
}

lookup types: they get the properties of an interface or type

interface Person {
name: string;
age: number;
location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string

// this next thing is very interesting,
// You can use this pattern with other parts of the type system to get type-safe lookups:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // Inferred type is T[K]
}

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
obj[key] = value;
}

let x = { foo: 10, bar: "hello!" };
let foo = getProperty(x, "foo"); // number
let bar = getProperty(x, "bar"); // string
let oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"
setProperty(x, "foo", "string"); // Error!, string expected number

indexed types:

interface Person {
name: string;
age: number;
location: string;
}

type P1 = Person["name"]; // string
type P2 = Person["name" | "age"]; // string | number
type P3 = string["charAt"]; // (pos: number) => string

type I1 = Person["age" | "name"]; // string | number
type I2 = Person[keyof Person]; // string | number | boolean

type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName]; // string | boolean

// VERY IMPORTANT!!
Another example of indexing with an arbitrary type is using number to get the type of an array’s elements. We can combine this with typeof to conveniently capture the element type of an array literal:

const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];

type Person = typeof MyArray[number]; // type Person = { name: string; age: number; }
type Age = typeof MyArray[number]["age"]; // type Age = number

// Or

type Age2 = Person["age"]; // type Age2 = number

built-in ts types: https://www.typescriptlang.org/docs/handbook/utility-]types.html#partialt

of which mapped type patterns are:

  • Partial<T>: constructs a type with all the properties of T set to optional
  • Required<T>: constructs a type with all the properties of T set to required
  • Readonly<T>: constructs a type with all the properties of T set to readonly
  • Record<K, T>: constructs a type with property names from K, where each property has type T
  • Pick<T, K>: constructs a type with just the properties from T specified by K
  • Omit<T, K>: constructs a type with all the properties from T except those specified by K

conditional types: Allow for a type to be set dynamically based on a provided condition. All conditional types follow the same format: T extends U ? X : Y (if T is assignable to U, then set the type to X. Otherwise, set the type to Y).

type AddOrConcat<T> = (x: T) => T extends number ? number : string

const addOrConcat: AddOrConcat<string|number> = (x) => {
if (typeof x === 'string') return String(x)
return Number(x)
}

// or if the "declare" syntax is easier:
declare function addOrConcat<T extends number | string>(x: T): T extends number ? number : string;

distributive conditional types:

type Example<T> = T extends string ? T : never;
type MyType = Example<'lolo'| 5 | { x: 'hey' }>

is the same as:

type Example<T> = ('lolo' extends string ? 'lolo' : never) | (5 extends string ? 5 : never) | ({ x: 'hey' } extends string ? { x: 'hey' } : never)

const heyThere: MyType // 'lolo' is the only valid value

// !! IMPORTANT: In order to create distributive conditional types "T" from "T extends" must be a "naked" type parameter (meaning a simple type, nothing like "string" or "boolean"), e.g:

type A<T> = string extends T ? "yes" : "no" // NOT DISTRIBUTIVE conditional type (string is not naked)

type B<T> = {x: T} extends {x: number} ? "yes" : "no" // NOT DISTRIBUTIVE conditional type ({x: T} in not naked either)

type C<T> = T extends string ? "yes" : "no" // THIS IS a conditional type (T is finally a naked generic type parameter)

infer: ability to declare generic types as part of a condition. Declares generic types inline.

// example:

function first<T extends [any, any]>(pair: T): T extends [infer U, infer U] ? U : any {
return pair[0];
}

// another example:

type SecondArg<T> = T extends (a: any, b: U) => any ? U : never;

type MyType = SecondArg<typeof Array['prototype']['slice']> // number | undefined (slice's second argument is number or undefined) VERY POWERFULL!!

type predicates: Use when we want to create a function that will filter blocks depending of the type of the parameter. Probably not the more useful thing of the world.

function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}

// Both calls to 'swim' and 'fly' are now okay.

let pet = getSmallPet();

if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}

private fields in classes: Note that TypeScript’s private modifier is not related to ECMAScript private fields, which are denoted with a hash sign (e.g., #privateField). Private TS fields are only private during compilation; at runtime they are accessible just like any normal class field. This is why the JavaScript convention of prefixing private fields with underscores is still commonly seen in TS code. ECMAScript private fields, on the other hand, have “hard” privacy and are completely inaccessible outside of a class at runtime.

enums: for the efficient representation of sets of constant values. They are REAL OBJECTS, NOT JUST TYPING CONSTRUCTS.

// example

enum Style {
NONE = 0,
BOLD = 1,
ITALIC = 2,
UNDERLINE = 4,
EMPHASIS = Style.BOLD | Style.ITALIC,
HYPERLINK = Style.BOLD | Style.UNDERLINE
}

// can be initialized with constants or via computed values, or they can be auto-initialized, or a mix of initializations. Note that
// auto-initialized entries must come before entries initialized with computed values.

enum Directions {
North, // will have value 0
South, // will have value 1
East = getDirectionValue(),
West = 10
}

ambient declarations (the "declare" keyword): mechanism for adding types to legacy and/or external code. One of the most common use cases for ambient types is to provide typings for entire modules or packages.

declare module "vetUtils" { // name of the package
export class Pet { // name of one of the exports
id: string;
name: string;
constructor(id: string, name: string);
}

export class Dog extends Pet { // name of the other export
bark(): void;
}
}

!! important: Assuming the declaration above was in a file vetUtils.d.ts that was included in a project, TypeScript would use the typings in the ambient declaration whenever a module imported resources from “vetUtils”. Note the d.ts extension. This is the extension for a declaration file, which can only contain types, no actual code. Since these files only contain type declarations, TypeScript does not generate compiled code for them.


types in try/catch statements

By default, values captured in a catch statement are defined as an any type.

try {
throw "error";
} catch (err) {
// err is "any" type
}

As of TypeScript 4, you can declare these values as unknown types, since that type fits better than any.

try {
throw "error";
} catch (err: unknown) {
// err is "unknown" type
}

In TypeScript 4.4, you can make the values captured in catch statements be defined as unknown by default by enabling the useUnknownInCatchVariables configuration option. This option is enabled by default when using the strict option.

compiler comments

  • // @ts-nocheck – A file with this comment at the top won’t be type checked
  • // @ts-check – When the checkJs compiler option isn’t set, .js files will be processed by the compiler but not type checked. Adding this comment to the top of a .js file will cause it to be type checked.
  • // @ts-ignore – Suppress any type checking errors for the following line of code
  • // @ts-expect-error – Suppress a type checking error for the following line of code. Raise a compilation error if the following line doesn’t having a type checking error.

Syntax for functions

classic functions:

function foo<T>(x: T): T { return x; }

arrow functions:

const foo = <T,>(x: T[]): T|undefined => x.pop(); // notice the comma!!!! (.ts files do not need the comma, .tsx will need it)

// or

const foo = { <T,>(x: T): T } = (x) => x

examples:

const comp3 = <T,>(arg: T[]): T|undefined => {
return arg.pop()
}

const comp = function<T>(arg: T[]): T|undefined {
return arg.pop()
}

const foo = <T,>(a: T) => a

Common tricks:

Convert array of strings to union type:

// 'as const' (const assertion) makes ts infer the array elements as "readonly"
// Without const assertion
const fruits = ['banana', 'apple', 'orange']
typeof fruits // string[]

// With const assertion
const fruits = ['banana', 'apple', 'orange'] as const
typeof fruits // 'banana' | 'apple' | 'orange'

Create a Union type from an Object's Values or Keys

typeof obj // {readonly name: "Bobby Hadz"; readonly country: "Chile"}
const obj = {
name: 'Bobby Hadz',
country: 'Chile',
} as const;

// type UValues = "Bobby Hadz" | "Chile"
type UValues = (typeof obj)[keyof typeof obj];

// type UKeys = "name" | "country"
type UKeys = keyof typeof obj;




Don't be shy, leave us a comment