Skip to content

Conditional type on a lookup always never #29211

Closed

Description

TypeScript Version: 3.3.0-dev.20181229
Search Terms:

  • conditional types
  • lookup
  • broken

Code

export interface PassthroughProps<
    DataType extends {[key: string]: any},
    DataTypeKey extends string = Extract<keyof DataType, string>,
    IdentityCandidate extends DataTypeKey = DataType[IdentityCandidate] extends string 
        ? IdentityCandidate 
        : never,
> {
    identityProperty: IdentityCandidate;
    label: DataTypeKey | ((option: DataType) => string);
}

interface MyType {
    id: string;
    name: string;
    times: number;
}

const myprops: PassthroughProps<MyType> = {
    // ITEM 1
    identityProperty: "times",
    label: "id",
};

Expected behavior:

Item 1

I'm expecting identityProperty to be ( "id" | "name" ), not never.

Actual behavior:

Item 1

I'm getting this error:

Type 'string' is not assignable to type 'never'.ts(2322)

Activity

  1. DanielRosenwasser commented on Dec 31, 2018

    @DanielRosenwasser
    Member

    I think that #29179 has broken this anyway since IdentityCandidate extends DataTypeKey = DataType[IdentityCandidate] references itself in its default. Can you confirm Anders Hejlsberg (@ahejlsberg)?

  2. atrauzzi commented on Dec 31, 2018

    @atrauzzi
    Author

    I think in my tinkering I've come up with variants that don't rely on such, so -- if there's a suitable alternative to make the example a bit more robust, let me know. 😄

    I should add -- without a circular reference, I'm not sure what other kind of syntax I'd have to come up with to express what I want here. The type is effectively justifying itself, or bailing out.

  3. ahejlsberg commented on Dec 31, 2018

    @ahejlsberg
    Member

    I think that #29179 has broken this anyway since IdentityCandidate extends DataTypeKey = DataType[IdentityCandidate] references itself in its default. Can you confirm Anders Hejlsberg (@ahejlsberg)?

    Yes, the code above now errors on the circular reference to IdentityCandidate in its own default (which is a good thing, since it doesn't make sense to reference a type parameter for which no type argument was specified).

  4. atrauzzi commented on Dec 31, 2018

    @atrauzzi
    Author

    Anders Hejlsberg (@ahejlsberg) - If that's the case, is there an alternate way for me to express:

    IdentityCandidate extends DataTypeKey = DataType[IdentityCandidate] extends string 
            ? IdentityCandidate 
            : never,
    

    I'm no fan of circular references, so I'm onboard there -- but I need a way to check if that type, when used as a key, retrieves a string. In this case, I'm not entirely convinced this is a circular reference, at least in intent.

  5. ahejlsberg commented on Dec 31, 2018

    @ahejlsberg
    Member

    Not entirely clear on what you're trying to do, but I think you could write it like this:

    export interface PassthroughProps<
        DataType extends {[key: string]: any},
        DataTypeKey extends string = Extract<keyof DataType, string>,
        IdentityCandidate = DataType[DataTypeKey] extends string ? DataType[DataTypeKey] : never,
    > {
        identityProperty: IdentityCandidate;
        label: DataTypeKey | ((option: DataType) => string);
    }

    However, it looks to me like the IdentityCandidate type parameter is never supposed to be specified and is just intended as an alias for the default value. That's definitely an anti-pattern, and I would suggest inlining it instead:

    export interface PassthroughProps<
        DataType extends {[key: string]: any},
        DataTypeKey extends string = Extract<keyof DataType, string>
    > {
        identityProperty: DataType[DataTypeKey] extends string ? DataType[DataTypeKey] : never;
        label: DataTypeKey | ((option: DataType) => string);
    }

    Hope that helps.

  6. atrauzzi commented on Jan 5, 2019

    @atrauzzi
    Author

    So, I think overall the advice here worked, but there's one thing that could possibly be improved:

    const key = item[this.props.identityProperty];

    When I do the above, the value key, doesn't want to be a string. Even though this.props.identityProperty is required to be a key that resolves to a string value.

    If I do this:

    const key = item[this.props.identityProperty!] as string;

    I get:

    Conversion of type 'ItemType[NonNullable<ItemType[Extract<keyof ItemType, string>] extends string ? Extract<keyof ItemType, string> : never>]' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

    I can do that, but I feel like the compiler has enough information here to know that the value I get when using this key will have to be a string?

  7. typescript-bot commented on Jul 13, 2019

    @typescript-bot
    Contributor

    This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

  8. locked as resolved and limited conversation to collaborators on Oct 21, 2025
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

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions