villasv
villasv

Reputation: 6841

How to assign fixed type value to partial of generic constrained type?

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.

Upvotes: 1

Views: 128

Answers (1)

Mirco S.
Mirco S.

Reputation: 2640

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

Upvotes: 1

Related Questions