How to create functions return some explicit types, and some implicit ones?

Viewed 40

Given the code below,

function system(): ISavable & ISerializable {
    return {
        num: 1, // error!

        save() {},
        load() {},

        serialize() {},
        deserialize() {},
    }
}

interface ISavable {
    save: () => void
    load: () => void
}

interface ISerializable {
    seriailze: () => void
    deserialize: () => void
}

Playground link →

The problem

When returning property num: 1, TypeScript complains it is not specified by either ISavable or ISerializable—and that's expected.

One possible solution would be creating one extra type that exposes all the stuff this function is going to return—but I'm looking for something that allows me to go implicit when it comes to the function's unique properties.

To make myself even clearer, when you don't specify the return of your function, the type of it is inferred and you get autocomplete, etc. I want that plus to be able to infer the types and guarantee some explicitly specified properties are returned as well.

Question

I'd like to type the return of my function in a way where it requires the function to return the properties from both interfaces (ISavable and ISerializable), but still allows me to return extra properties freely, that may be unique to this function.

That said,

  1. Is it possible?
  2. If so, how?

I think my needs can be achieved through classes, but I'm pursuing a solution specifically for functions.

2 Answers

Here is a slightly different approach:

The desire to create objects which satisify a type while still maintaining their own literal type is not new. There is already a PR to add a satisfies operator to the language. But until it is added, we can create our own "operator".

const satisfies = <T,>() => <S extends T>(arg: S) => arg

This generic function can be used inside system.

function system() {
    return satisfies<ISavable & ISerializable>()({
        num1: 1,
        num2: 2,

        save() {},
        load() {},

        serialize() {},
        deserialize() {},
    })
}

system().num1
system().num2

The satisfies function ensures that the constraint is fulfilled and extra properties are allowed and will be observable in the return type.

Playground

Just for the record, the cleanest solution I can imagine to this problem was found by the OP, (loosely) based on an answer I hasted to post and then deleted as I realized it was seriously lacking; for what it's worth, his was posted as a message to my first answer, before the fine solution by Tobias.

function system() {
    const ret: ISavable & ISerializable = {
        save() {},
        load() {},

        serialize() {},
        deserialize() {},
    }

    return { num: 100, ...ret }
}

interface ISavable {
    save: () => void
    load: () => void
}

interface ISerializable {
    serialize: () => void
    deserialize: () => void
}

system().num

Inspired by the solution of Tobias, I tried to find a version that:

  • has zero effect in the generated javascript code,
  • produces a more relevant TS error message when the ISavable & ISerializable extension requirement is not met.

The best version I could come up with is this:

type ExtendsP<I, T extends I> = T

function system(){
    return {
        num: 100,

        save() {},
        load() {},

        serialize() {},
        deserialize() {},
    };
}

type _ = ExtendsP<() => ISavable & ISerializable, typeof system>

interface ISavable {
    save: () => void
    load: () => void
}

interface ISerializable {
    serialize: () => void
    deserialize: () => void
}

system().num

It is somewhat awkward and might result in a linter warning for the lack of use of type _ (I doubt many ts linters are familiar with the convention for _ in js), but I hope someone might find something relevant for their similar issues.

Related