microsoft / microsoft/TypeScript

TypeScript 4.1+: Generic binding too broad in recursive conditional types

Open
#41,380 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Needs Investigation
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
2d 4h
Merged PRs (30d)
132

Description

TypeScript Version: 4.2.0-dev.20201103

Search Terms: Recursive Conditional types, generics, tuple types

Code

// A test object
const obj = {
    a: '2',
    b: {
        c: 3,
        d: {
            e: 'string',
            f: {
                g: {
                    h: 4
                }
            }
        }
    }
} as const

// its type
type Struct = typeof obj;

/**
 * First recursive conditional type 
 * 
 * CheckArguments gets 
 * Obj - A nested object
 * Arugments - A tuple of keys to go down a nested path
 * 
 * The type is recursive, I check if the current argument lists extends keyof Obj --> Then the recursion ends
 * Otherwise, I check if the first value in the tuple is keyof Obj, infer the rest, and go down the same type
 * again
 */
type CheckArguments<Obj, Arguments> = 
    Arguments extends [keyof Obj] ? Obj[Arguments[number]] :
    Arguments extends [keyof Obj, ...infer U] ? CheckArguments<Exclude<Obj[keyof Obj], string | number>, U> : never;

/**
 * Tests, all 👍
 */
type Foo = CheckArguments<Struct, ['a']> // "2"
type Foo2 = CheckArguments<Struct, ['b', 'd', 'e']> // "string"
type Foo3 = CheckArguments<Struct, ['b', 'd', 'f', 'g', 'h']> // 4

/**
 * Second recursive conditional type
 * 
 * Arguments gets
 * Obj - A nested Obj
 * 
 * The recursive conditional type creates a union type of possible nested arguments in a tuple
 * This is based on Anders example from TSConf: https://github.com/ahejlsberg/tsconf2020-demos/blob/master/template/main.ts
 * (Dotted paths)
 */
type Arguments<Obj> = 
    Obj extends object ?
        [keyof Obj] | SubArguments<Obj, keyof Obj> :
        never;

// A helper type
type SubArguments<Obj, Key> =  Key extends keyof Obj ? [Key, ...Arguments<Obj[Key]>] : never;

// For example, the possible tuples of Struct 👍
type Bar = Arguments<Struct>;

// equals to this union type
type Bar2 = ["a" | "b"] | ["b", "c" | "d"] | ["b", "d", "e" | "f"] | ["b", "d", "f", "g"] | ["b", "d", "f", "g", "h"]

/**
 * So both recursive conditional types work on their own. A problem is once I want to combine
 * them in a function, where I expect the first argument to bind to a value type within the Arguments union
 * 
 * Instead of having just one value type passed to CheckArguments (the one that is bound through the generic Keys),
 * TypeScript passes all parts of the union to CheckArguments. This leads CheckArguments to return all possible values
 */

declare function get<Obj extends object, Keys extends Arguments<Obj>>(o: Obj, ...keys: Keys): CheckArguments<Obj, Keys>

/** 
 * Tests 💥
 * */
const foo = get(obj, 'a') // Should be "2" 😢 
const foo1 = get(obj, 'b', 'c') // Should be  3 😢 
const foo2 = get(obj, 'b', 'd', 'e') // Should be "string" 😢 
const foo3 = get(obj, 'b', 'd', 'f', 'g', 'h') // Should be 4 😢 

Expected behavior: Keys gets bound to the value type passed as an argument to the function. This value type is then used for CheckArguments

Actual behavior: Keys is the entire union type Arguments<Obj>, not the subset. This leads to CheckArguments returning a too broad return type (and taking very long to evaluate ;-))

Playground Link: Click here

Related Issues: Did not find any.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the generic inference case in the linked TypeScript Playground using the reported 4.2.0-dev.20201103 version, and compare the inferred types of foo through foo3 with the expected results. Done means each call binds Keys to the supplied tuple rather than the full Arguments union, while avoiding the excessive evaluation described.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.