Skip to content

T is distributed in nested conditional [T] extends [infer U] ? U extends unknown ? ... #34504

@DanielRosenwasser

Description

@DanielRosenwasser

Minimal repro:

type Oops<T> =
    [T] extends [infer U]
        ? (U extends unknown ? [U, T] : never)
        : never;

type X = Oops<"hello" | "world">;

Actual

type X = ["hello", "hello"] | ["world", "world"]

Expected

type X = ["hello", "hello" | "world"] | ["world", "hello" | "world"]

Use-case:

I want to define a type Exclusify that defines mutually exclusive properties, much in the same way that object literal type normalization does. So

type Foo = Exclusify<"hello" | "world", number>;

should be equivalent to

type Foo =
    | { hello: number, world: undefined }
    | { hello: undefined, world: number };

I can do this by defining Exclusify with a generic default to avoid distributing on Keys.

type Exclusify<Keys extends keyof any, V, SingleKey = Keys> =
    // Distribute on SingleKey
    SingleKey extends unknown
        ? { [K in Keys]: K extends SingleKey ? V : undefined }
        : never;


type Foo = Exclusify<"hello" | "world", string>

This works! But I'd rather not use a generic with a default. Instead, I figured I could add a conditional type.

type Exclusify<Keys extends keyof any, V> =
    // Introduce SingleKey
    [Keys] extends [infer SingleKey]
        // // Distribute on SingleKey
        ? SingleKey extends unknown
            ? { [K in Keys]: K extends SingleKey ? V : undefined }
            : never
        : never

type Foo = Exclusify<"hello" | "world", string>

This, uh, doesn't work? It gives me

type Foo =
    | { hello: string; }
    | { world: string; }

Re-introducing SingleKey as a defaulted type parameter and replacing the infer SingleKey with unknown fixes it.

type Exclusify<Keys extends keyof any, V, SingleKey = Keys> =
    [Keys] extends [unknown]
        // // Distribute on SingleKey
        ? SingleKey extends unknown
            ? { [K in Keys]: K extends SingleKey ? V : undefined }
            : never
        : never

type Foo = Exclusify<"hello" | "world", string>

It doesn't make sense to me why [Keys] gets distributed in the presence of an infer.

Metadata

Metadata

Labels

Working as IntendedThe behavior described is the intended behavior; this is not a bug

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions