TypeScript Creating Union Type from a Union Type

type TodoAction =
    | { type: 'INIT' }
    | { type: 'ADD_TODO', text: string }
    | { type: 'REMOVE_TODO', id: string }
    | { type: 'SET_COMPLETED', id: string };
type KeysOfUnion<T extends { type: any }> = 
   T extends { type: infer K } ? {} extends Omit<Extract<T, { type: K }>, 'type'> ? never : K : never;

function dispatch(actionType: KeysOfUnion<TodoAction>) { throw new Error('Not implemented') };

dispatch('ADD_TODO');
Continue reading

TypeScript Separate Union Type Fields

My aim is to create a function which has two parameters. First one is union value of type field in Action and second parameter should force developer to use specific object in Action.

For example, assume that we have this function function dispatch(actionType, payload){}

dispatch('SIGN_OUT', { type: 'SIGN_OUT', userName: '' });

or

dispatch('SIGN_IN_FAILURE', { type: 'SIGN_IN_FAILURE', error: '' });

type Action =
    | { type: 'INIT' }
    | { type: 'SYNC' }
    | { type: 'SIGN_UP', userName: string, password: string, attributeList: Array<any> }
    | { type: 'SIGN_IN', userName: string, pasword: string }
    | { type: 'SIGN_IN_SUCCESS', accessToken: string }
    | { type: 'SIGN_IN_FAILURE', error: string }
    | { type: 'SIGN_OUT', userName: string }
    | { type: 'FORGOT_PASSWORD', userName: string }
    | { type: 'FORGOT_PASSWORD_SUCCESS', verificationCode: string }
    | { type: 'CHANGE_PASSWORD', userName: string, oldPassword: string, newPassword: string }
    | { type: 'CONFIRM_REGISTRATION', userName: string, confirmationCode: string }
    | { type: 'RESEND_CONFIRMATION_CODE', userName: string }
    | { type: 'DELETE_USER', userName: string }
    | { type: 'GET_SESSION', userName: string }
    | { type: 'GET_SESSION_SUCCESS', session: string };

type TodoAction =
    | { type: 'INIT' }
    | { type: 'ADD_TODO', text: string }
    | { type: 'REMOVE_TODO', id: string }
    | { type: 'SET_COMPLETED', id: string };
Continue reading

TypeScript listen property value changes

Creating makeWatchedObject function with overloading

type PropEventSource<T> = {
    on<K extends string & keyof T>(eventName: `${K}Changed`, callback: (newValue: T[K]) => void): void;
};

type ReturnType<T1> = T1 & PropEventSource<T1>;

function makeWatchedObject<T1>(obj1: T1): ReturnType<T1>
function makeWatchedObject<T1, T2>(obj1: T1, obj2: T2): ReturnType<T1 & T2>
function makeWatchedObject<T1, T2, T3>(obj1: T1, obj2: T2, obj3: T3): ReturnType<T1 & T2 & T3>
function makeWatchedObject<T1, T2, T3>(obj1: T1, obj2?: T2, obj3?: T3): any {

    const events = new Map<string, (a: any) => void>();

    const on_: PropEventSource<T1 & T2 & T3> = {
        on(eventName: string, callback: (newValue: any) => void): void {

            events.set(eventName, callback);
        }
    }
    const obj = { ...obj1, ...obj2, ...obj3, ...on_ };

    const proxyObj = new Proxy(obj, {
        set: function (target, property, value, receiver) {

            const event = events.get(`${property as string}Changed`);

            if (event) {
                event(value);
                return true;
            }

            return Reflect.set(target, property, value, receiver);
        }
    });

    return proxyObj;
}

Usage

const personInfo = { id: 1, firstName: 'kenan', lastName: 'hancer', age: 36 };
const personAddress = { country: 'UK', city: 'London' };
const personContact = { email: 'kh@kh.com', phone: '077111-33-33' };

let person = makeWatchedObject(personInfo, personAddress, personContact);


// works! 'newName' is typed as 'string'
person.on("firstNameChanged", (newName: string) => {

    console.log(`new name is ${newName.toUpperCase()}`);
});


// works! 'newAge' is typed as 'number'
person.on("ageChanged", (newAge: number) => {
    if (newAge < 0) {
        console.log("warning! negative age");
    }
});

person.age = 34;

TypeScript Function Conditional Type Good Usage

Example 1

type MessageOf<T> = T extends { message: infer MessageType } ? MessageType : never;

const getMessage = <T extends { message: any }>(obj: T): MessageOf<T> => obj.message;



const sms = { phone: '07733838', message: ['Hello World', 'Merhaba Dunya'] };

const smsMessage = getMessage(sms); // string[]

const email = { from: 'kh@kh.com', to: 'kkkk@kkkk.com', message: 'Hello World' };

const emailMessage = getMessage(email); // string

Example 1.1

Notice that line 3 in below code, T extends { message: unknown } message field data type is unknown so returning value from function should be casted to any like obj.message as any

type MessageOf<T> = T extends { message: infer MessageType } ? MessageType : never;

const getMessage = <T extends { message: unknown }>(obj: T): MessageOf<T> => obj.message as any;



const sms = { phone: '07733838', message: ['Hello World', 'Merhaba Dunya'] };

const smsMessage = getMessage(sms); // string[]

const email = { from: 'kh@kh.com', to: 'kkkk@kkkk.com', message: 'Hello World' };

const emailMessage = getMessage(email); // string

TypeScript Conditional Types Good Usage Part 1

Example 1

I try to understand eventType field from Event object. But in order to use createEventGridMessage in other TypeScript file, you need to import StartEvent, InfoEvent and EndEvent types. So next example of this case looks better to me 🙂

import {v4 as uuidv4} from 'uuid';

export type BaseEvent = {
    timeStamp?: number,
    componentName: string,
    componentVersion: string,
    operationId: string,
    correlationId: string,
    data: any,
}

export type StartEvent = Partial<BaseEvent> & {
    eventType: 'LogStart',
    source: string,
    trigger: string,
};

export type EndEvent = Partial<BaseEvent> & {
    eventType: 'LogEnd'
    destination: 'Event Grid' | 'Http Response' | 'Database' | string,
};

export type InfoEvent = Pick<EndEvent, keyof Omit<EndEvent, 'eventType'>> & { eventType: 'LogInfo' };

export type Event = StartEvent | EndEvent | InfoEvent;


export type EventGridMessageParameters<T extends Event> = Pick<EventGridMessage<T>, 'data' | 'eventType' | 'subject' | 'topic'>;

export type EventType<T extends Event> = T extends StartEvent ? 'LogStart' : T extends EndEvent ? 'LogEnd' : T extends InfoEvent ? 'LogInfo' : 'UNKNOWN EVENT TYPE';

export interface EventGridMessage<T extends Event> {
    id: string;
    topic: string;
    subject: string;
    eventType: EventType<T>;
    eventTime: Date;
    data: T;
    dataVersion: string;
    metadataVersion: string;
}

export const createEventGridMessage = <T extends Event>({
                                                            data,
                                                            eventType,
                                                            subject = "New message from unknown source",
                                                            topic = ""
                                                        }: EventGridMessageParameters<T>): EventGridMessage<T> =>
    ({
        id: uuidv4(),
        topic,
        subject,
        eventType,
        eventTime: new Date(),
        data: {...data, timeStamp: data.timeStamp || new Date().getTime()},
        dataVersion: "1.0",
        metadataVersion: "1.0"
    });


const startLogMessage = createEventGridMessage<StartEvent>({
    eventType: 'LogStart',
    data: {
        eventType: 'LogStart',
        trigger: '',
        source: '',
        correlationId: '',
        componentVersion: '',
        componentName: '',
        operationId: ''
    },
    subject: '',
    topic: ''
});
console.log(startLogMessage);


const infoLogMessage = createEventGridMessage<InfoEvent>({
    eventType: 'LogInfo',
    data: {
        eventType: 'LogInfo',
        correlationId: '',
        componentVersion: '',
        componentName: '',
        operationId: '',
        destination: '',
    },
    subject: '',
    topic: ''
});
console.log(infoLogMessage);


const endLogMessage = createEventGridMessage<EndEvent>({
    eventType: 'LogEnd',
    data: {
        eventType: 'LogEnd',
        timeStamp: new Date().getTime(),
        correlationId: '',
        componentVersion: '',
        componentName: '',
        operationId: '',
        destination: '',
    },
    subject: '',
    topic: ''
});
console.log(endLogMessage);
Continue reading

TypeScript Conditional Types

Conditional Type Expression

SomeType extends OtherType ? TrueType : FalseType;

Basic Usages

Example 1

type NumberOrNot<T> = T extends number ? string : never;

Example 2

type StringOrNot<T> = T extends string ? string : never;

Example 3

type BooleanOrNumberOnly<T> = T extends boolean | number ? T : never;

type NewType = StringOrNumberOnly<string | number | boolean>;

const a1: NewType = true; // boolean

const a2: NewType = 23; // number
Continue reading

Jest Mocking Default Instance

Example 1

const mockedPutObject = jest.fn();

jest.mock('@aws-sdk/client-s3', () => {
    return {
        S3: jest.fn(() => ({
            putObject: () => mockedPutObject()
        }))
    }
});

Example 2

jest.mock('uuid', () => ({ v4: () => '00000000-0000-0000-0000-000000000000' }));

Example 3

jest.mock('uuid');

const mockedUuidv4 = uuidv4 as jest.Mock;

mockedUuidv4.mockReturnValue('00000000-0000-0000-0000-000000000000');

Example 4

const mockedAjvValidate = jest.fn().mockReturnValue(true);

jest.mock('ajv', () => {
    return jest.fn(() => ({
        compile: () => () => mockedAjvValidate()
    }))
});

Example 5

jest.mock('moment', () => () => ({ format: () => '30-12-2021' }));

Example 6

import { v4 as uuid } from 'uuid';

export interface IEvent {
    id?: string;
    eventType: string;
    data: any;
}

export interface IEventRepository {
    getEvent(id: string): IEvent | undefined;
    setEvent(event: IEvent): IEvent;
}

const createEvent = (event: IEvent): IEvent => ({ id: uuid(), ...event });

export class EventRedisRepository implements IEventRepository {

    constructor(private events: Array<IEvent> = []) { }

    getEvent(id: string): IEvent | undefined {
        return this.events.find(item => item.id === id);
    }
    setEvent(event: IEvent): IEvent {

        const newEvent: IEvent = createEvent(event);

        this.events.push(newEvent);

        return newEvent;
    }
}

export class EventCosmosDbRepository implements IEventRepository {

    constructor(private events: Array<IEvent> = []) { }

    getEvent(id: string): IEvent | undefined {
        return this.events.find(item => item.id === id);
    }
    setEvent(event: IEvent): IEvent {

        const newEvent: IEvent = createEvent(event);

        this.events.push(newEvent);

        return newEvent;
    }
}

import { Persister } from '../src';
import { EventRedisRepository, EventCosmosDbRepository, IEventRepository, IEvent } from './sample';

const mockedRedisGetEvent = jest.fn();

const mockedRedisSetEvent = jest.fn();

const mockedCosmosdbGetEvent = jest.fn();

const mockedCosmosdbSetEvent = jest.fn();

jest.mock('./sample', () => {
    return {
        EventRedisRepository: jest.fn(() => ({
            getEvent: () => mockedRedisGetEvent(),
            setEvent: () => mockedRedisSetEvent(),
        })),
        EventCosmosDbRepository: jest.fn(() => ({
            getEvent: () => mockedCosmosdbGetEvent(),
            setEvent: () => mockedCosmosdbSetEvent(),
        })),
    }
});

const eventRedisRepository = new EventRedisRepository();

const eventCosmosdbRepository = new EventCosmosDbRepository();

const module_name = `${Persister.name}`;

describe(`${module_name} Test`, () => {
    it('should call setEvent function in both persisters', () => {

        const eventPersister = new Persister<IEventRepository>([eventRedisRepository, eventCosmosdbRepository]);

        const eventId = '4e0c71bb-eedf-43dd-b9a1-911a7ed6d74f';
        const newEvent: IEvent = { id: eventId, eventType: 'Submission', data: {} };

        eventPersister.set('setEvent', op => op(newEvent));

        expect(mockedRedisSetEvent).toHaveBeenCalledTimes(1);

        expect(mockedCosmosdbSetEvent).toHaveBeenCalledTimes(1);

    });
});

Example 6 – updated and short version

import { Persister } from '../src';
import { EventRedisRepository, EventCosmosDbRepository, IEventRepository, IEvent } from './sample';
import { mocked } from 'jest-mock'

jest.mock('./sample');

const eventRedisRepository = new EventRedisRepository();

const eventCosmosdbRepository = new EventCosmosDbRepository();

const mockedEventRedisRepository = mocked(eventRedisRepository, true);

const mockedEventCosmosdbRepository = mocked(eventRedisRepository, true);

const module_name = `${Persister.name}`;

describe(`${module_name} Test`, () => {
    it('should call setEvent function in both persisters', () => {

        const eventPersister = new Persister<IEventRepository>([eventRedisRepository, eventCosmosdbRepository]);

        const eventId = '4e0c71bb-eedf-43dd-b9a1-911a7ed6d74f';
        const newEvent: IEvent = { id: eventId, eventType: 'Submission', data: {} };

        eventPersister.set('setEvent', op => op(newEvent));

        expect(mockedEventRedisRepository.setEvent).toHaveBeenCalledTimes(1);

        expect(mockedEventCosmosdbRepository.setEvent).toHaveBeenCalledTimes(1);

    });
});