Search code examples
typescripttypestypescript-typings

Typescript type for object with optional functions


I have a set of objects in a typescript project that I want to create a type for. Each object implements some combination of three functions a, b, and c. I would like to create a type such that when I create an object of this type you are restricted to only being able to implement the functions a, b, and c. When used, based on the specific implementation you can only call the actually implemented functions

An interface with three functions does not work as each function must be implemented. If I make the functions optional this works but then when I actually use an object I can call a, b, or c regardless of whether they're implemented or not

Consider the following example, I would like all of the following examples to share a type that allows them to implement any combination of the functions a, b, and c. But then when used are restricted to be being able use the functions they actually implemented

e.g. exampleOne.a(); is fine exampleOne.c(); would be a compile time error

const exampleOne = {
  a: () => { console.log('a') },
  b: () => { console.log('b') },
}

const exampleTwo = {
  c: () => { console.log('c') },
}

const exampleThree = {
  b: () => { console.log('b') },
  c: () => { console.log('c') },
}

Solution

  • For the specific examples in the question, you may be looking for a type with optional functions combined with the newish satisfies operator. Here's the type:

    interface Example {
        a?: () => void;
        b?: () => void;
        c?: () => void;
    }
    

    Then here's using it with those example objects:

    const exampleOne = {
        a: () => {
            console.log("a");
        },
        b: () => {
            console.log("b");
        },
    } satisfies Example;
    
    exampleOne.a(); // No error
    exampleOne.b(); // No error
    exampleOne.c(); // Error
    
    const exampleTwo = {
        c: () => {
            console.log("c");
        },
    } satisfies Example;
    
    exampleTwo.a(); // Error
    exampleTwo.b(); // Error
    exampleTwo.c(); // No error
    
    const exampleThree = {
        b: () => {
            console.log("b");
        },
        c: () => {
            console.log("c");
        },
    } satisfies Example;
    
    exampleThree.a(); // Error
    exampleThree.b(); // No error
    exampleThree.c(); // No error
    

    That works because the type of exampleOne, etc., is a subtype of Example that removes the optionality from the functions that are provided.

    That will only work for situations where you have that subtyping. If you just have Example, you need to prove first that the functions exist:

    function example(e: Example) {
        // Calling one of the functions using optional chaining, which will call the
        // function if it exists, skip it (no call, no error) if not
        e.a?.();
    
        // Calling one of the functions by explicitly checking if it's there
        if (e.b) {
            e.b();
        }
    
        // You can't just call the function, because it's not clear whether it exists
        e.c(); // <== Error
    }
    

    Playground with all of the above