Skip to content

Recursive mapped type inference not working as expected #24318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
massimonewsuk opened this issue May 22, 2018 · 7 comments
Closed

Recursive mapped type inference not working as expected #24318

massimonewsuk opened this issue May 22, 2018 · 7 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@massimonewsuk
Copy link

massimonewsuk commented May 22, 2018

TypeScript Version: 2.9.1-insiders.20180521

Search Terms:
recursive inference

Code

type Entity<T> = T & { _: "Entity" };

type MyNestedType = {
  something0: number;
  level1?: {
    something1: number;
    level2?: {
      something2: number;
      level3?: {
        something3: number;
        property: string;
      };
    };
  };
  level1Array?: Entity<
    {
      something1: number;
      level2Array?: Entity<
        {
          something2: number;
          level3Array?: Entity<
            {
              something3: number;
              property: string;
            }[]
          >;
        }[]
      >;
    }[]
  >;
};

type EverythingPresent<T> = { [P in keyof T]-?: EverythingPresent<T[P]> };
declare var everythingPresent: EverythingPresent<MyNestedType>;
everythingPresent.level1;
everythingPresent.level1.level2;
everythingPresent.level1.level2.level3;
everythingPresent.level1Array[0].something1;
everythingPresent.level1Array[0].level2Array[0].something2;
everythingPresent.level1Array[0].level2Array[0].level3Array[0].something3;

type OnlyEntitiesPresent<T> = {
  [P in keyof T]-?: T[P] extends Entity<infer U> | undefined
    ? Entity<
        U extends Array<infer R>
          ? Array<OnlyEntitiesPresent<R>>
          : OnlyEntitiesPresent<U>
      >
    : T[P]
};
declare var onlyArraysPresent: OnlyEntitiesPresent<MyNestedType>;
onlyArraysPresent.level1;
onlyArraysPresent.level1.level2;
onlyArraysPresent.level1.level2.level3; // This errors as expected
onlyArraysPresent.level1Array[0].something1;
onlyArraysPresent.level1Array[0].level2Array[0].something2; // This should not error
onlyArraysPresent.level1Array[0].level2Array[0].level3Array[0].something3; // This should not error

Expected behavior:
I should only get an error where expected (1 line)

Actual behavior:
I get errors on the last 2 lines as well

Just to note - in 2.8.3 I also get errors for the last 3 lines of everythingPresent - so some improvements have been made but it's still not as expected

@massimonewsuk
Copy link
Author

massimonewsuk commented May 22, 2018

Here are some examples where it works as expected

// Normal nested stuff
type Node = {
    always: {
        property: string;
    }
    maybe?: {
        property: string;
    }
    leaf?: Node;
};

type EverythingPresent<T> = { [P in keyof T]-?: EverythingPresent<T[P]> };
declare var everythingPresent: EverythingPresent<Node>;
everythingPresent.maybe.property;
everythingPresent.leaf.maybe.property;
everythingPresent.leaf.leaf.maybe.property;
everythingPresent.leaf.leaf.leaf.maybe.property;

type OnlyLeafsPresent<T> = {
  [P in keyof T]-?: T[P] extends Node
    ? OnlyLeafsPresent<Node>
    : T[P]
};
declare var onlyLeafsPresent: OnlyLeafsPresent<Node>;
// These examples work fine
onlyLeafsPresent.maybe.property;
onlyLeafsPresent.leaf.maybe.property;
onlyLeafsPresent.leaf.leaf.maybe.property;
onlyLeafsPresent.leaf.leaf.leaf.maybe.property;

// Generic nested stuff
type GenericNode<T> = {
    always: {
        property: string;
    }
    maybe?: {
        property: T;
    }
    leaf?: GenericNode<T>;
};
declare var everythingPresent2: EverythingPresent<GenericNode<string>>;
everythingPresent2.maybe.property;
everythingPresent2.leaf.maybe.property;
everythingPresent2.leaf.leaf.maybe.property;
everythingPresent2.leaf.leaf.leaf.maybe.property;

type OnlyLeafsPresentGeneric<T> = {
    [P in keyof T]-?: T[P] extends GenericNode<infer U>
      ? OnlyLeafsPresentGeneric<GenericNode<U>>
      : T[P]
  };
declare var onlyLeafsPresent2: OnlyLeafsPresentGeneric<GenericNode<string>>;
// These examples work fine
onlyLeafsPresent2.maybe.property;
onlyLeafsPresent2.leaf.maybe.property;
onlyLeafsPresent2.leaf.leaf.maybe.property;
onlyLeafsPresent2.leaf.leaf.leaf.maybe.property;

@massimonewsuk massimonewsuk changed the title Recursive generic parameter inference not working Recursive mapped type inference not working as expected May 22, 2018
@ghost
Copy link

ghost commented May 22, 2018

Note: since Node has all-optional properties, everything extends it, so the conditional types will always use the true branch. Could you update your example to avoid that pitfall?

@ghost ghost added the Needs More Info The issue still hasn't been fully clarified label May 22, 2018
@massimonewsuk
Copy link
Author

massimonewsuk commented May 22, 2018

@Andy-MS Thanks, that's cleared things up a bit... it seems that the Array is a special case where things aren't quite working as expected... the examples with Node all work fine now

@ghost
Copy link

ghost commented May 22, 2018

It looks like you can fix that by changing T[P] extends Array<infer U> to T[P] extends Array<infer U> | undefined -- since T[P] still contains undefined even though you removed optionality from the property.

@massimonewsuk
Copy link
Author

You're right - I could have sworn I tried that and it never helped.

I've updated my original code example to what I actually wanted to achieve.

It seems to work fine in 2.9.1 so I'm closing the issue. Sorry for taking your time!

@ghost ghost added Question An issue which isn't directly actionable in code and removed Needs More Info The issue still hasn't been fully clarified labels May 22, 2018
@massimonewsuk
Copy link
Author

@Andy-MS so I've basically replicated the issue I was originally facing. If you had a moment to take a look, here's the code example that I'm having issues with.

// Entities
type Order = {
    id: number;
    customer?: Entity<Customer>;
    lineItems?: Entity<OrderLineItem[]>;
};

type Customer = {
    name: string;
    bestFriend?: Entity<Customer>;
    legacyData?: {
        registered: boolean;
    };
};

type OrderLineItem = {
    quantity: number;
    options?: Entity<OrderLineItemOption[]>;
    item?: Entity<Item>;
};

type OrderLineItemOption = {
    colour: string;
};

type Item = {
    description: string;
};

// Type helpers
type PropertiesWithoutNevers<T, U> = {
    [P in keyof T]: T[P] extends U ? never : P
}[keyof T];

type Mandatory<T> = { [P in keyof T]-?: T[P] };

type Entity<T> = T & { _: "Entity" };

type EagerLoadedRelations<TEntity> = Pick<
  Mandatory<TEntity>,
  PropertiesWithoutNevers<
    {
      [P in keyof TEntity]-?: TEntity[P] extends Entity<infer TRelation> | undefined
        ? Entity<TRelation extends Array<infer TOneToMany> ? Array<Eager<TOneToMany>> : Eager<TRelation>>
        : never
    },
    TEntity
  >
>;

type NormalFields<TEntity> = Pick<
  TEntity,
  PropertiesWithoutNevers<
    { [P in keyof TEntity]-?: TEntity[P] extends Entity<infer U> | undefined ? never : TEntity[P] },
    TEntity
  >
>;

type Eager<T> = EagerLoadedRelations<T> & NormalFields<T>;


// Without Eager<T> - everything works as expected
declare var order: Order;
order.id; // works as expected
order.customer.name; // error as expected
order.lineItems[0].quantity; // error as expected

// With Eager<T> - Eager<T> doesn't seem to apply deeper than 1 level
declare var eagerOrder: Eager<Order>;
eagerOrder.id; // works as expected
eagerOrder.customer.name; // works as expected
eagerOrder.customer.bestFriend.name; // ERROR - doesn't work - bestFriend should be eager loaded
eagerOrder.customer.legacyData.registered; // errors as expected
eagerOrder.lineItems[0].quantity; // works as expected
eagerOrder.lineItems[0].item.description; // ERROR - doesn't work - items should be eager loaded
eagerOrder.lineItems[0].options[0].colour; // ERROR - doesn't work - options should be eager loaded

@ghost
Copy link

ghost commented May 22, 2018

Some things I noticed:

  • In the above example, it looks like NormalFields can be removed without affecting the erroring examples.
  • EagerLoadedRelations is a Pick. Pick doesn't have any deep effects, it just picks certain fails. Mandatory also doesn't work deeply, so it will only have an effect on first-level fields, not eagerOrder.customer.bestFriend.
  • The type of values in the first argument to PropertiesWithoutNevers only matters to the extent that it extends TEntity or not. You can even see when hovering over eagerOrder (after removing NormalFields) that it's just var eagerOrder: Pick<Mandatory<Order>, "customer" | "lineItems">.

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

1 participant