Skip to content

Generic typing inference composition work as variable not as function #30808

@MasGaNo

Description

@MasGaNo

TypeScript Version: 3.4.2

Search Terms:
Generic variable vs generic function
Generic composition
Generic inference parameter in function

Code
Let's consider this code

/**
 * Extract object props by a specific value
 */
type PickByValue<T, ValueType> = Pick<T, {
  [Key in keyof T]: T[Key] extends ValueType ? Key : never
}[keyof T]>;

/**
 * Map keys by value-compatibility. Non-recursive.
 */
type MatchKeys<T, U> = {
  [P in keyof T]: keyof PickByValue<U, T[P]>;
};

/**
 * Like keyof, but for value
 */
type ValueOf<A extends Object> = Exclude<A[keyof A], undefined>;

/**
 * The src props we have
 */
export interface SrcPropsInterface {
  srcAttributeString: string;
  srcAttributeNumber: number;
}

/**
 * The dest props we want to apply
 */
export interface DestPropsInterface {
    destAttributesString: string;
    anotherAttributesString: string;
    destAttributesNumber: number;
    anotherAttributesNumber: number;
}

In a variable approach, the following code works perfectly with all completion:

/**
 * Here it's perfectly constraint
 */
type Combi = Partial<MatchKeys<SrcPropsInterface, DestPropsInterface>>;
const comb: Combi = {
    srcAttributeNumber: "anotherAttributesNumber",
    srcAttributeString: 'destAttributesString',
    // toto: 'test' // Error as expected
};

image

With a generic function approach, we get some weird things:

declare const obj: DestPropsInterface;

// Try generic way + inference
/**
 * Return a object that exposed the missing attributes that still need to be fill somewhere
 */
function map<
    DestProps,
    T extends Partial<MatchKeys<SrcPropsInterface, DestProps>>
    >(data: DestProps, map: T) {

    // Consider the Proxy code here
    const obj = {} as Pick<DestProps, Exclude<keyof DestProps, ValueOf<T>>>;
    return obj;
}
Case 1: no completion anymore
const target = map(obj, {
});

image
But as soon as I write blindly a known attribute from SrcPropsInterface, the type checking work:

const target = map(obj, {
    srcAttributeNumber: 'anotherAttributesNumber',
    srcAttributeString: 'badValue' // error as expected
});

image

Case 2: any-like

We can put any kind of value and there is no complaint (and naturally no completion):

const target = map(obj, {
    srcAttributeNumber: 'anotherAttributesNumber',
    foo: 'bar',
    bar: 42
});
Case 3: no output anymore

As soon as the map object is correctly fill, the target object get correctly all remaining attribute:

const target = map(obj, {
    srcAttributeNumber: 'anotherAttributesNumber',
});
target.anotherAttributesString; // Ok
target.destAttributesNumber; // Ok
target.destAttributesString; // Ok
target.anotherAttributesNumber; // Error as expected: we already remove it

image
But if we provide a wrong map because the type checker doesn't provide the correct feedback, target is converted as never:

const target = map(obj, {
    srcAttributeNumber: 'anotherAttributesNumber',
    foo: 'bar'
});

image

Expected behavior:
Case 1: get correct type checker in the map parameters like for the variable approach
Case 2: I suppose it's just a side-effect of Case 1, but I expect to have TypeScript wiping me by attempting to introduce unknown parameters.
Case 3: I suppose it's just a side-effect of Case 1 and 2.

Actual behavior:
Check Case 1, Case 2 and Case 3: no type checking on the generic map parameter whereas in a variable approach, it works.

Playground Link:
Playground @ 2019-04-08

Related Issues:
#28938

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions