Skip to content
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

Conditional types break with property chaining #32735

Open
AnyhowStep opened this issue Aug 6, 2019 · 7 comments
Open

Conditional types break with property chaining #32735

AnyhowStep opened this issue Aug 6, 2019 · 7 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@AnyhowStep
Copy link
Contributor

TypeScript Version: 3.5.1

Search Terms:

property chaining, generic, object type param, conditional type

Code

Modified version of @fatcerberus ' attempt to break TS.

type Droste<T extends {x:number|string}> = {
    value: T,
    /**
     * Should alternate between string and number
     */
    droste: Droste<{x:(T["x"] extends number ? string : number)}>
};

declare const droste: Droste<{x:number}>;

//number
const _0 = droste.droste.droste.droste.droste.droste.droste.droste.droste.droste.droste
    .droste.droste.droste.droste.droste.droste.droste.droste.droste.droste
    .droste.droste;
const _1 = _0.droste; //string
const _2 = _1.droste; //number
const _3 = _2.droste; //string

const _4 = _3.droste; //Expected number, actual string, ???
const _5 = _4.droste; //string
const _6 = _5.droste; //string
const _7 = _6.droste; //string
const _8 = _7.droste; //string
const _9 = _8.droste; //string
//string forever and ever. Where is `number`?

Expected behavior:

Each access to .droste should alternate between string and number.
Or give a max instantiation depth/count error.

Actual behavior:

It alternates between string and number and then breaks after while.
From that point, it just sticks with string.

No errors given.

Playground Link:

Playground

Related Issues:

It is similar to #32707
Except, it gives up and resolves the type to string, rather than any.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Aug 6, 2019

I know, I know, recursive type alias, bad.

Interfaces break, too,

interface Droste<T extends {x:number|string}> {
    value: T,
    /**
     * Should alternate between string and number
     */
    droste: Droste<{x:(T["x"] extends number ? string : number)}>
};

declare const droste: Droste<{x:number}>;

//number
const _0 = droste.droste.droste.droste.droste.droste.droste.droste.droste.droste.droste
    .droste.droste.droste.droste.droste.droste.droste.droste.droste.droste
    .droste.droste;
const _1 = _0.droste; //string
const _2 = _1.droste; //number
const _3 = _2.droste; //string

const _4 = _3.droste; //number
const _5 = _4.droste; //Expected string, actual number, ???
const _6 = _5.droste; //number
const _7 = _6.droste; //number
const _8 = _7.droste; //number
const _9 = _8.droste; //number
//number forever and ever. Where is `string`?

Playground

It breaks in a different way, though.

@webstrand
Copy link
Contributor

webstrand commented Aug 6, 2019

Looks like it's falling afoul of the instantiation limit

type check<T> = T extends Droste<{ x: string }> ? true : false;
type a = check<typeof _1>; // true
type b = check<typeof _2>; // false 
type c = check<typeof _3>; // Type instantiation is excessively deep and possibly infinite.

@AnyhowStep
Copy link
Contributor Author

It seems like the workaround outlined in #32707 (comment) works,

/**
 * Uses the workaround discovered here,
 * https://github.com/microsoft/TypeScript/issues/32707#issuecomment-518347966
 * 
 * Now, everything resolves correctly.
 */
type NextDrosteFix<X extends number|string> = (
    Droste<{x:(X extends number ? string : number)}>
);
type NextDroste<T extends {x:number|string}> = (
    NextDrosteFix<T["x"]>
);
interface Droste<T extends {x:number|string}> {
    value: T,
    /**
     * Should alternate between string and number
     */
    droste: NextDroste<T>
};

declare const droste: Droste<{x:number}>;

//number
const _0 = droste.droste.droste.droste.droste.droste.droste.droste.droste.droste.droste
    .droste.droste.droste.droste.droste.droste.droste.droste.droste.droste
    .droste.droste;
const _1 = _0.droste; //string
const _2 = _1.droste; //number
const _3 = _2.droste; //string

const _4 = _3.droste; //number
const _5 = _4.droste; //string
const _6 = _5.droste; //number
const _7 = _6.droste; //string
const _8 = _7.droste; //number
const _9 = _8.droste; //string

Playground

And, as always, I know it works but I don't know why it works.

@jack-williams
Copy link
Collaborator

If I had to guess (and it really is a guess), there is a caching of instantiations done on type id. With your original example you create a new object literal type (with a new id) for each recursive instantiation that will have no cached value.

In the modified example each the recursion goes through NextDrosteFix which is instantiated with a primitive type (and primitive types are interned). NextDrosteFix will be cached for the instantiations at string and number.

@AnyhowStep
Copy link
Contributor Author

AnyhowStep commented Aug 6, 2019

Are array types also considered primitive types for interning purposes?

Array types never seem to give problems. Only object literal types.

[Edit]

Object literal types and interfaces and classes (when used as a type param constraint)

@jack-williams
Copy link
Collaborator

I think arrays and tuples are cached, but I would wait on confirmation.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Aug 7, 2019

Truman hits the edge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants