1

I have a basic interface for things that may have Ids:

interface Identifiable {
    id?: number;
}

And I have a generic function that converts a record object into a thing with id:

function fromRowToObj1<T extends Identifiable>(row: { id: number; }): Partial<T> {
    return { id: row.id };
    // Type '{ id: number; }' is not assignable to type 'Partial<T>'.
}

I understand that this happens because there are Ts that extend Identifiable that would make that return statement illegal. For example, the type { id: undefined } or { id: 1 }. So I decided to tweak the return type a bit to enforce a numeric id:


type Identified<T extends Identifiable> = {
    [K in keyof T]?: K extends "id" ? number : T[K];
}
// Should give something like type C = { id?: number | undefined; ... }

function fromRowToObj2<T extends Identifiable>(row: { id: number; }): Identified<T> {
    return { id: row.id };
    // Type '{ id: number; }' is not assignable to type 'Identified<T>'.
}

Why, though? Which possible T (such that T extends Identifiable) makes it so { id: number } is not assignable to Identified<T>?

If there's no way to adjust the Identified type to make this work, is there another way to type the conversion function to work with generic subtypes of Identifiable?

Link to playground.

1 Answer 1

1

The issue you are facing is thoroughly described here. As you noticed yourself there are subtypes of T extends Identifiable which renders your return value { id: row.id } invalid. For example Identified<{id?: never}> will never be valid for { id: row.id }. Never is still a valid type for id because you declared all keys of Identified as optional. Identified<T> is actually equal to Partial<T> if T extends Identifiable. Typescript correctly throws an error here. Though, you can still work around that if you set valid default values from which you can work onwards (playground):

interface Identifiable {
    id?: number;
}

// results in optional id
function fromRowToObj1<T extends Identifiable>(row: { id: number; }) {
    const result: Partial<T> = {} // valid for all subtypes of Partial<T>
    result.id = row.id
    return result;
}

// results in non optional id
function fromRowToObj2<T extends Identifiable>(row: { id: number; } ) {
    const partial: Partial<T> = {}; // valid for all subtypes of Partial<T>
    const result = {
        ...partial,
        id: row.id
    };
    return result;
}

interface TestObject {
    id: number,
    arg1: string;
    arg2: boolean;
}

const result1 = fromRowToObj1<TestObject>({id: 5});
result1.id // optional
result1.arg1 = "test" // intellisense works
result1.arg2 = true; // intellisense works

const result2 = fromRowToObj2<TestObject>({id: 5});
result2.id // not optional
result2.arg1 = "test" // intellisense works
result2.arg2 = true; // intellisense works
Sign up to request clarification or add additional context in comments.

3 Comments

As long as id from Identifiable is optional, these workarounds have types that can break them. I've posted an example for such a type in the playground. But these are types that probably won't be used anyway.
The playground says that Identifiable<{ id?: never}> is { id?: number | undefined}, though. So why isn't { id: number } assignable to it?
Anyway, your second example with non-optional id works perfectly. With that I can type the return type as Partial<T> & { id: number } which is what I wanted. It does feel kinda weird to have to go through that hoop.

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.