Skip to content

Using undefined as a discriminator within a mapped type yields erroneous types when primitive intersections are involved #14471

Open

Description

TypeScript Version: 2.2.0

Code

// A *self-contained* demonstration of the problem follows...
type DeepReadonly<T> = {
    readonly [K in keyof T]: DeepReadonly<T[K]>;
}

interface FieldBrand {
    " do not use ": void;
}

type FieldId = number & FieldBrand;

interface DefOne {
    field: string | FieldId;
    kind: string;
}

interface DefTwo {
    field?: undefined; // Allow discriminant checks on 'field'
    value: string;
}

type Def = DefOne | DefTwo;

interface State {
    a?: Def;
    b?: Def;
}

type ROState = DeepReadonly<State>;

function lookupName(f: FieldId): string {
    return "";
}

function remapFieldsToNames(channels: ROState): ROState {
    const newState: State = {};
    for (const k of Object.keys(channels)) {
        const key = k as keyof ROState;
        const ch = channels[key];
        let replacement: ROState[typeof key] | undefined = undefined;
        if (ch) {
            if (ch.field) {
                const f = ch.field;
                if (typeof f === "number") {
                    f; // Should be FieldId or number, not never!
                    replacement = { ...ch, field: lookupName(f) };
                }
                else if (typeof f === "string") {
                    f; // correct
                }
            }
        }
        newState[k] = replacement || channels[k];
    }
    return newState;
}

Expected behavior:
There are no type errors in the above, and f after the typeof f === "number" check is either a number or FieldId.

Actual behavior:
replacement = { ...ch, field: lookupName(f) }; has a type error and f is of type never.

Activity

  1. weswigham commented on Mar 5, 2017

    @weswigham
    MemberAuthor

    There's also some odd shenanigans going on with DeepReadonly leaving off its generic arguments in the quickinfo within the function body which may be related/should be looked into:
    image

  2. weswigham commented on Mar 5, 2017

    @weswigham
    MemberAuthor

    The weirdness with DeepReadonly and a tagged number actually cause a type incombatibility every time I try to assign the deeply readonly structure to the normal one.

  3. denis-migdal commented on Apr 8, 2026

    @denis-migdal

    Isn't the issue on the DeepReadonly type definition ?

    Using the following type definition seems to solves this issue on the last TS version:

    type DeepReadonly<T> = {
        readonly [K in keyof T]: T[K] extends {} ? DeepReadonly<T[K]> : T[K];
    }

    Cf Playground Link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs InvestigationThis issue needs a team member to investigate its status.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions