1

I have a use-case where an external query returns an object with a property with the same name as one of my interfaces. As you can see in the sample executeQuery function, if I pass in "message" as a query, then I will be returned an object with 1 property named "Message".

I want to be able to create a generic interface of T that has 1 property where the name is the name of T and the type is T.

I understand there are run-time solutions to this, but I was wondering if this was possible using only Typescript types at compile time.

shared code:

function executeQuery<T>(query: "message" | "mailbox") {
    const data = query === "message" ?
        { Message: { id: 1 } } as unknown as T :
        { Mailbox: { id: 2 } } as unknown as T
    return { data: data }
}

interface Message {
    id: number
}

interface Mailbox {
    id: number
}

1st solution:

interface AllContainer {
    Message: Message
    Mailbox: Mailbox
}

const messageQueryResult = executeQuery<AllContainer>("message")
console.log(messageQueryResult.data.Message.id)

const mailboxQueryResult = executeQuery<AllContainer>("mailbox")
console.log(mailboxQueryResult.data.Mailbox.id)

2nd solution:

interface MessageContainer {
    Message: Message
}

interface MailboxContainer {
    Mailbox: Mailbox
}

const messageQueryResult2 = executeQuery<MessageContainer>("message")
console.log(messageQueryResult2.data.Message.id)

const mailboxQueryResult2 = executeQuery<MailboxContainer>("mailbox")
console.log(mailboxQueryResult2.data.Mailbox.id)

What I want to be able to do:

interface GenericContainer<T> {
    [T.Name]: T  // invalid Typescript
}

const messageQueryResult3 = executeQuery<GenericContainer<Message>>("message")
console.log(messageQueryResult3.data.Message.id)

const mailboxQueryResult3 = executeQuery<GenericContainer<Mailbox>>("mailbox")
console.log(mailboxQueryResult3.data.Mailbox.id)

2 Answers 2

3

First of all, I'm going to add some distinguishing properties to the Message and Mailbox types. TypeScript's type system is structural and not nominal, so if both Message and Mailbox have the same exact structure, the compiler will consider them the same types, despite their having different names. So let's just do this to avoid potential issues:

interface Message {
    id: number,
    message: string; // adding distinct property
}

interface Mailbox {
    id: number,
    mailbox: string; // distrinct property
}

And because the type system isn't nominal, it really doesn't care about the names you give to types or interfaces. So there isn't any handle the compiler can give you to extract the name of an interface, even at compile time.

If you're looking for compile time solutions, you're going to have to refactor things. Type names are ignored, but key names of objects are not (since property keys exist at runtime and two types with different keys are really different types). So you can maybe start with an AllContainer-like type instead:

interface AllContainer {
    Message: {
        id: number,
        message: string;
    }
    Mailbox: {
        id: number,
        mailbox: string;
    }
}

And instead of referring to the type as Message, you'd refer to it as AllContainer["Message"]. You can go further and more strongly type your executeQuery() function, with better type inference for callers (while still needing a type assertion in the implementation):

interface QueryMap {
    message: "Message",
    mailbox: "Mailbox"
}


function executeQuery<K extends keyof QueryMap>(query: K) {
    const data = (query === "message" ?
        { Message: { id: 1 } } :
        { Mailbox: { id: 2 } }) as any as Pick<AllContainer, QueryMap[K]>
    return { data: data }
}


const messageQueryResult = executeQuery("message")
console.log(messageQueryResult.data.Message.id)

const mailboxQueryResult = executeQuery("mailbox")
console.log(mailboxQueryResult.data.Mailbox.id)

That all compiles... the QueryMap interface gives the compiler a handle on how the parameter to executeQuery() is related to the property of AllContainer you want to talk about.

Anyway, hope that gives you some idea of how to proceed. Good luck!

Sign up to request clarification or add additional context in comments.

Comments

2

One of the ways how you can solve this is by using "function overloads".

You basically make 2 signatures, 1 for a "message" response, and 1 for a "mailbox" response:

interface Message {
    id: number
}

interface Mailbox {
    id: number
}

interface Container<T> {
    data: T;
}

function executeQuery(name: 'message'): Container<{ Message: Message }>;
function executeQuery(name: 'mailbox'): Container<{ Mailbox: Mailbox }>;
function executeQuery(name: string): Container<any>; // Fallback string signature
function executeQuery(name: string): Container<any> { // Implementation signature, not visible
    switch(name) {
        case 'message': {
            const res: Container<{ Message: Message }> = {
                data: {
                    Message: {
                        id: 1,
                    },
                },
            };
            return res;
        }
        case 'mailbox': {
            const res: Container<{ Mailbox: Mailbox }> = {
                data: {
                    Mailbox: {
                        id: 1,
                    },
                },
            };
            return res;
        }
        default:
            throw new Error('Cannot execute query for: ' + name);
    }
}

const messageQueryResult3 = executeQuery("message")
console.log(messageQueryResult3.data.Message.id)

const mailboxQueryResult3 = executeQuery("mailbox")
console.log(mailboxQueryResult3.data.Mailbox.id)

This implementation is the best used when defining the types for an external type-less system, as it is quite easy to make a mistake inside this system because of the use of any in its return type, but when using this, it significantly becomes easier, as you don't need to pass any type to the function, and you get the correct return type back.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.