question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Recursive conditional types are aliased

See original GitHub issue

I am trying to create a conditional type that converts

type Before = {
  a: string;
  b: number;
  c: string | undefined;
  d: number | undefined;
  nested: {
    a2: string;
    b2: number;
    c2: string | undefined;
    d2: number | undefined;
    nested2: {
      a3: string;
      b3: number;
      c3: string | undefined;
      d3: number | undefined;
    };
  };
};

to

type After = {
  a: string;
  b: number;
  c?: string | undefined;
  d?: number | undefined;
  nested: {
    a2: string;
    b2: number;
    c2?: string | undefined;
    d2?: number | undefined;
    nested2: {
      a3: string;
      b3: number;
      c3?: string | undefined;
      d3?: number | undefined;
    };
  };
};

When I hover on After in the below code


const fnBefore = (input: Before) => {
  return input;
};

const fnAfter = (input: After) => {
  return input;
};

it shows

type After = {
    c?: string | undefined;
    d?: number | undefined;
    a: string;
    b: number;
    nested: Flatten<{
        c2?: string | undefined;
        d2?: number | undefined;
    } & {
        a2: string;
        b2: number;
        nested2: Flatten<{
            c3?: string | undefined;
            d3?: number | undefined;
        } & RequiredProps>;
    }>;
}

instead of properly converted type.

According to #22011

If an conditional type is instantiated over 100 times, we consider that to be too deep. At that point, we try to find the respective alias type that contains that conditional type.

only more complex types should be aliased, but I always face this issue. Also for simple types:

type Simple = {
  nested: {
    a2: string;
    c2: string | undefined;
  };
};

TypeScript Version: 2.8.0-dev.201180314

Full Code

type Before = {
  a: string;
  b: number;
  c: string | undefined;
  d: number | undefined;
  nested: {
    a2: string;
    b2: number;
    c2: string | undefined;
    d2: number | undefined;
    nested2: {
      a3: string;
      b3: number;
      c3: string | undefined;
      d3: number | undefined;
    };
  };
};

type Simple = {
  nested: {
    a2: string;
    c2: string | undefined;
  };
};

type Flatten<T> = { [K in keyof T]: T[K] };

type OptionalPropNames<T> = { [P in keyof T]: undefined extends T[P] ? P : never }[keyof T];
type RequiredPropNames<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];

type OptionalProps<T> = { [P in OptionalPropNames<T>]: T[P] };
type RequiredProps<T> = { [P in RequiredPropNames<T>]: T[P] };

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

type ConvertObject<T> = Flatten<MakeOptional<OptionalProps<T>> & RequiredProps<T>>;

type DeepConvertObject<T> = ConvertObject<{ [P in keyof T]: DeepConvert<T[P]> }>;

type DeepConvert<T> = T extends object ? DeepConvertObject<T> : T;

type After = DeepConvert<Before>;
type SimpleAfter = DeepConvert<Simple>;

const fnBefore = (input: Before) => {
  return input;
};

const fnAfter = (input: After) => {
  return input;
};

Expected behavior: The tooltip should be shown without aliases.

Actual behavior: The tooltip is shown with aliases.

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:2
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
akutruffcommented, Feb 9, 2021

@RyanCavanaugh

As per your request for suggestions on heuristic improvements, would the TS team be open to user supplied compiler hints for when the user is dealing with complex recursive conditional types?

The work around for this issue is the same for https://github.com/microsoft/TypeScript/issues/28508#issuecomment-775904459 which is to flatten the hierarchy with another recursive conditional. That actually put the user at even more risk of hitting https://github.com/microsoft/TypeScript/issues/34933#. (I’m running typescript@next and keep hitting the infinite recursion problem.)

Similar to the the example here: https://github.com/microsoft/TypeScript/issues/28508#issue-380418556, and above, my code follows the same pattern:

//NOTE: Details of this don't matter for this thread, but users are seemingly converging on this pattern 
//       for mapping hierarchies.  
export type ShapeMapper<T> =  { [P in keyof T]: Shape<T[P]> };

export type Shape<TDefinition> =
    TDefinition extends LiteralType<infer LiteralKind>
    ? LiteralKind
    : TDefinition extends Type<'string'>
    ? string
    : TDefinition extends Type<'number'>
    ? number
    : TDefinition extends Type<'boolean'>
    ? boolean
    : TDefinition extends Type<'bigint'>
    ? bigint
    : TDefinition extends Type<'null'>
    ? null
    : TDefinition extends Type<'undefined'>
    ? undefined
    : TDefinition extends ArrayType<infer ElementKind>
    ? Array<Shape<ElementKind>>
    : TDefinition extends UnionType<infer KeyKind>
    ? Shape<KeyKind>
    : TDefinition extends ObjType<infer TShapeDefinition>
    ? ShapeMapper<TShapeDefinition> 
    : never;

Then we use the template on our actual type schemas. The following is the pattern used by many if not all the TypeScript validation libs, like io-ts, zod, etc. All of our application types are defined in this inverted way.

//Define schema
const Person = t.obj({
    name: t.str,
});

const Address = t.obj({
    owner: Person
});

//Infer the TypeScript type
type PersonShape = Shape<typeof Person>;
type AddressShape = Shape<typeof Address>;

But, this destroys intellisense as relations emerge. (The Address[owner] property is unreadable) So we use this work around

export type FlattenForIntellisense<T> = T extends object ? {} & { [P in keyof T]: FlattenForIntellisense<T[P]> } : T;

type PersonShape = FlattenForIntellisense<Shape<typeof Person>>;
type AddressShape = FlattenForIntellisense<Shape<typeof Address>>;

Proposal?

As the hierarchies start to grow deeper, the problem gets worse while the domain types like Person, and Address are natural caching points for the compiler to heuristically prune the search space. These domain objects are also where we want intellisense to simplify generics. The compiler doesn’t know that Person and Address are where it can cache.

Instead, what if we give the compiler a hint to know when to fully compute and simplify a type, and then treat that type like an explicit type declaration from now on? From then on, the compiler and IDE treats Person and Address like as if the user had entered them as text in a .ts file. Intellisense would also ignore the underlying templates that defined the template driven domain type, and always show the most simplified form with recursion fully computed.

Some options:

Option 1: A new intrinsic. Pick a good name, I suck at naming.

type PersonShape = ResolveTypeFully<Shape<typeof Person>>;
type AddressShape = ResolveTypeFully<Shape<typeof Address>>;

Option 2: Whenever a type alias derives from an interface, then the type is fully inferred, and the compiler fully resolves the type as described above. Since I believe an interface can only extend a type that can be fully computed, then maybe this a natural spot. However, this is very verbose, and makes a weird empty interface in the code without clear purpose. Also, if the compiler ever does relax this requirement, then it breaks.

interface PersonShape extends Shape<typeof Person> { }
1reaction
lchimarucommented, May 7, 2019

@BetterCallSky have you tried just simply flattening the type? Look at this:

type DeepConvert<T> = T extends object ? { [P in keyof DeepConvertObject<T>]: DeepConvertObject<T>[P] } : T;

I know, it’s showing following error

Type 'P' cannot be used to index type 'ConvertObject<{ [P in keyof T]: DeepConvert<T[P]>; }>'.

but surprisingly it works as it should. Take a look on hovering effect: Zrzut ekranu z 2019-05-07 14-11-54

Read more comments on GitHub >

github_iconTop Results From Across the Web

Recursive Types alias in typescript - typing flatten function
The answer is that even if the recursion in types is allowed it's only in a subset of cases. Namely when using a...
Read more >
Notes on TypeScript: Recursive Type Aliases and Immutability
Now that we learned about the recursive type aliases, let's create an immutable type that we can use to add more guarantees into...
Read more >
Documentation - TypeScript 4.1
In TypeScript 4.1, conditional types can now immediately reference themselves within their branches, making it easier to write recursive type aliases.
Read more >
Announcing TypeScript 4.1 Beta - Microsoft Developer Blogs
... Types; Key Remapping in Mapped Types; Recursive Conditional Types ... and the in-progress pull request to switch to type alias helpers.
Read more >
Match Types - Scala 3 - EPFL
Match types can form part of recursive type definitions. ... Conditional types only reduce if both the scrutinee and pattern are ground, whereas...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found