Bence Mányoki
Bence Mányoki

Reputation: 317

TypeScript failes to infer return type of constrained generic function in some cases, why is this happening?

I have this code, which works just fine in a simple case, meaning the "permissionList" const is type inferred, VSCode can suggest me the properties, such as "permission_1". Also inside the "constraintFn", when I declare the object from which the type is inferred, it suggests me "code" and "id", since these are the valid properties that you can use based on the generic type "T".

type PropType<T> = {
    [key: string]: T;
};

type Codes = 'code1' | 'code2';

class PermissionType {
    code: string;
    id: number;
}

const constraintFn = <T, TRe extends PropType<T>>(t: { new(): T; }, obj: TRe): TRe => {

    return obj;
};

export const permissionList = constraintFn(PermissionType, ({
    permission_1: {
        code: 'code1',
        id: 1,
    },
    permission_2: {
        code: 'code2',
        id: 2,
    },
}));

Now, if I change one thing:

class PermissionType {
    code: Codes; // this changed from string, to Codes
    id: number;
}

The inference still works when I declare the object in the function param, I can chose from a list of Codes, BUT the inference of the const "permissionList" disappears, and it only displays:

PropType<PermissionType> 

Instaead of:

{
    permission_1: {
        code: string;
        id: number;
    };
    permission_2: {
        code: string;
        id: number;
    };
}

Thanks, I hope there's a solution to this. It would be fun to create these kind of types.

Upvotes: 1

Views: 52

Answers (3)

Bence M&#225;nyoki
Bence M&#225;nyoki

Reputation: 317

My solution so far is this. I don't really consider this as a perfect one, since it involves some kind of weird type recursion (see how TRe is constrained by a type that accepts TRe as a generic parameter), which feels like a hack. Anyways, if it helps someone, this compiles fine.

[edit]: reflecting on the answer containing "as const", that's something I don't want to write on the end of each object that I supply as parameter to the "constraintFn". I much rather find a solution that does this inside the function.

type PropConstraintType<TObj, TProp> = {
    [K in keyof TObj]: TProp;
};

type Codes = 'code1' | 'code2';

class PermissionType {
    code: Codes;
    id: number;
}

const constraintFn = <T, TRe extends PropConstraintType<TRe, T>>(t: { new(): T; }, obj: TRe): TRe => {

    return obj;
};

export const permissionList = constraintFn(PermissionType, ({
    permission_1: {
        code: 'code1',
        id: 1,
    },
    permission_2: {
        code: 'code2',
        id: 2,
    },
}));

Upvotes: 1

Bishwajit jha
Bishwajit jha

Reputation: 384

When we use generics for type alias, the Typescript compiler generally doesn't go to infer types at each of the levels, hence we just see the alias PropType<PermissionType>

There is a trick using which we can make this type expand with a custom type alias Id<T>

type Id<T> = {} & {[k in keyof T]: T[k]}


// This type allows the expansion of other type 
type Id<T> = {} & {[k in keyof T]: T[k]}

const constraintFn = <T, TRe extends PropType<T>>(t: { new(): T; }, obj: TRe): Id<TRe> => {
    return obj;
};

export const permissionList = constraintFn(PermissionType, {
    permission_1: {
        code: "code1",
        id: 1,
    },
    permission_2: {
        code: 'code2',
        id: 2,
    },
});

// Now permission list has types => { [x: string]: PermissionType }

Code Playground

Upvotes: 0

tenshi
tenshi

Reputation: 26324

Odd that using as const here makes TypeScript give you the whole type...

export const permissionList = constraintFn(PermissionType, {
    permission_1: {
        code: "code1",
        id: 1,
    },
    permission_2: {
        code: 'code2',
        id: 2,
    },
} as const); // added here

Playground

Upvotes: 1

Related Questions