Skip to content

Types

Mapped types

When you don’t want to repeat yourself, sometimes a type needs to be based on another type.

You can extend the name, remove readonly modifier, change types, ... generally map from one type property to another.

type Events = {
    add:  string;
    delete: string;
}

type OnEvents = {
    [Property in keyof Events as `on${Capitalize<Property>}`] : () => any;
}

const userActions: OnEvents = {
    onAdd: () => {},
    onDelete: () => {}
}

// access type of property
type IsString = Events['add'];
//    ^? 

// keyof
type EventKeys = keyof Events;
//    ^? 

const invalidKey: EventKeys = 'move';


type Paths<T> = T extends object ?
    { [K in keyof T]-?: [K] | [K, ...Paths<T[K]>]  }[keyof T]
    : never;

type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
    };

Template literals

Template literal types build on string literal types, and have the ability to expand into many strings via unions.

type ChessLetters = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H';
type ChessNumbers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;

type BoardPositions = `${ChessLetters}${ChessNumbers}`[];

const b: BoardPositions = ['A1'];

type RgbCssType = `rgb(${number},${number},${number})`;

const rgbWrong: RgbCssType = 'asdf';
const rgbCorrect: RgbCssType = 'rgb(1,1,1)';

Never

type IsAnythingAssignableToNever = boolean extends never ? true : false;
//    ^?

type IsNeverAssignableToAnything = never extends boolean ? true : false;
//    ^?


type Locations = 'Oslo' | 'London';
//    ^?


// keep js usage save
function getValidLocationCountry(location: Locations): string {
    switch (location) {
        case 'Oslo':
            return 'Norway';
        case 'London':
            return 'England';
        default: 
            throw new Error(`${location} not known`);
    }
}
getValidLocationCountry("Oslo");

//compile
function getValidLocationCountryCompile(location: Locations): string {
    switch (location) {
        case 'Oslo':
            return 'Norway';
        case 'London':
            return 'England';
        default: 
            const exhaustiveCheck: never = location; // now the compiler complains when we extend the Locations enum an pass in the new value.
            throw new Error(`${location} not known`);
    }
}

getValidLocationCountryCompile('Oslo');

type NoEmptyString<T extends string> = T extends '' ? never : T;

function isEmptyString<T extends string>(nonEmpty: NoEmptyString<T>) {

}

isEmptyString('');

infer

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = MyReturnType<typeof add>;
//   ^?


type RgbInfer<T> = T extends `rgb(${infer first},${infer second},${infer third})`
? [first, second, third] : never;

type RgbInferNumber<T> = T extends `rgb(${infer first extends number},${infer second extends number},${infer third extends number})`
? [first, second, third] : never;

type ValidRgb = RgbInfer<'rgb(1,2,3)'>;
//    ^?
type ValidRgbNumber = RgbInferNumber<'rgb(1,2,3)'>;
//    ^? 


type InvalidRgb = RgbInferNumber<'rgb(1,2,a)'>;
//    ^? 

Recursive

// builtin type: Awaited

type Resolved = Awaited<Promise<Promise<Promise<string>>>>;
//    ^? 

type Tupel<Type, Length extends number, Tupel extends Type[] = []> = Tupel['length'] extends Length ? Tupel : TupelSized<Type, Length, [Type, ...Tupel]>;


type T3 = Tupel<number, 4>;
//   ^?

// MAX depth 1000 

interface Form {
    a: string;
    b: {
        c: {
            d: string;
        }
    }
}

type ArrayOrNever<T> = T extends any[] ? T : never;

type Path<T> = T extends object ? {[Key in keyof T]: [Key] | [Key, ...Path<T[Key]>]}[keyof T] : never;
type R = Path<Form>;
//   ^?

Branded types

The idea is to create a more specific and unique data type with greater clarity and specificity, this is accomplished by adding attributes or labels to an existing type to create a new, more specific type.

// simple version

type Email = string & {__brand: 'Email'}; // general pattern for branded types

function isValidEmail(input: string): input is Email {
    ...
}

function sendEmail(input: Emai) {

}

if(isValidEmail(email)){
    sendEamil(email); // only compiles when "IsValidEmail" was called first
}


// more complex
function branded<PrimitiveType, BrandName, Brand extends PrimitiveType & {__brand: BrandName}>(
    checkFn: (input: PrimitiveType) => boolean, brandName: BrandName
): [(input: PrimitiveType) => input is Brand, Brand] {
    const isBrand = function (input: PrimitiveType): input is Brand {
        return checkFn(input);
    }

    return [isBrand, {} as Brand];
}

const [isEmail, EmailObject] = branded((input: string) => true, 'ValidEmail' as const);

type ValidEmail = typeof EmailObject;

function sendEmail(input: ValidEmail) {

}

const email = 'mail';
if(isEmail(email)) {
    const validEmail: ValidEmail = email;
    sendEmail(email);
}

const invalidEmail: ValidEmail = email;