Search code examples
typescripttypeguards

Typescript mapped Type Guard


i have function that can get multiple types as argumet (in my case "get" proxy handler) and that argumet should be processed diferently depends on its type.

for plain js i know how to deal with it, something like this:

const calls = {
    "String": (str)=>{/*...*/},
    "Number": (num)=>{/*...*/},
    "Symbol": (symb)=>{/*...*/},
    "Object": (obj)=>{/*...*/}
}
function foo(obj){
    const name = Object.getPrototypeOf(obj).constructor.name;
    if (name in calls) {
        calls[name](obj);
    } else {
    /*unknow type*/
    }
}

but in typescript in terms to keep type track, all that comes to mind is is type guards

// isNumber, isString, isSymbol, isObject - type guards
function foo(obj: string | number | symbol):void {
    if (isString(obj)){
    /*...*/
    } else if (isNumber(obj)) {
    /*...*/
    } else if (isSymbol(obj)) {
    /*...*/
    } else if (isObject(obj)) {
    /*...*/
    } else {
    /*unknow type*/
    }
}

but in compare with plain js its look silly (will fast grow out of control and also make alot of unnecessary operations), so:

is there any equivalent solutions for TS?

example 2.0:

class Container {
    constructor(){}
    metaData = {};
    props = new Map();
}

const valuePreProcess = {
    "Number"(target, prop, value){ return value.toString() },
    "Container"(target, prop, value){ value.metadata.parent = target; return value; }
}

const handlers = {
    set(target, prop, value){
        //define type or may be inner class of passed 'value' to process it right 
        const processedValue = valuePreProcess[<typeOfValue>](target, prop, value);
        target.props.set(prop, processedValue);
    }
}

const obj = new Proxy(new Container(),handlers);

probably use prototype itself as key

const valuePreProcessors = new Map([
    [Number.prototype,(target, prop, value)=>value.toString()],
    [Container.prototype,(target, prop, value)=>{ value.metadata.parent = target; return value; }]
]);

const handlers = {
    set(target, prop, value){
        const processFunc = valuePreProcessors.get(Object.getPrototypeOf(value));
        target.props.set(prop, processFunc(target, prop, value));
    }
}

Solution

  • as result i used objects prototype as identifier

    class SomeClass {/* ... */};
    
    type TypeOfMap = [
        typeof Number["prototype"],
        typeof String["prototype"],
        typeof Object["prototype"],
        typeof SomeClass["prototype"]
    ];
    
    function getProto<T extends TypeOfMap[number]>(obj: T): T{
        return Object.getPrototypeOf(obj);
    }
    
    abstract class HandlersMap extends Map{
        abstract set<K extends TypeOfMap[number]>(key: K, value: (value: K)=>void): this
        abstract get<K extends TypeOfMap[number]>(key: K): ((value: K)=>void) | undefined;
    }
    
    const Handlers = new Map() as HandlersMap;
    Handlers.set(Number.prototype,(val)=>{});
    Handlers.set(Object.prototype,(val)=>{});
    Handlers.set(String.prototype,(val)=>{});
    Handlers.set(SomeClass.prototype,(val)=>{});
    
    function foo(obj: TypeOfMap[number]){
        const handler = Handlers.get(getProto(obj));
        if (handler){
            handler(obj);
        } else {
            //unexpected type
        }
    }