镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Inference inconsistency with string keys #26257

Closed

Description

@yortus

TypeScript Version: 3.1.0-dev.20180807

Search Terms:
string index inference, string key inference

Code

const SYM = Symbol();
type StringProps1<T> = T extends {[x: string]: infer U} ? U : never;
type StringProps2<T> = T extends {[K in Extract<keyof T, string>]: infer U} ? U : never;

type T1 = StringProps1<{a: 'aaa', [SYM]: 'sym'}>;   // T1 = 'aaa' | 'sym'
type T2 = StringProps2<{a: 'aaa', [SYM]: 'sym'}>;   // T2 = 'aaa'

Expected behavior:
I expected StringProps1 and StringProps2 to mean the same thing (infer from just the string keys).

Accordingly, T1 and T2 would both inferred as 'aaa', both excluding the symbol key.

Actual behavior:

T2 excludes the symbol key as expected, but T1 includes it, as shown in the code comments.

Even if StringProps1 and StringProps2 did have subtly different meanings, it doesn't seem logical to include the [SYM]: 'sym' as part of the string indexer, since it doesn't belong there.

Related Issues:

This is a simpler repro of #26183, although that issue shows its nothing specific to conditional types. It seems to be just how string indexers work with type inference.

The search terms above brought up #25065 and #24560, but they don't look like the same issue.

Activity

  1. ghost added
    BugA bug in TypeScript
    on Aug 7, 2018
  2. ghost added on Aug 7, 2018
  3. ghost removed on Aug 7, 2018
  4. ahejlsberg commented on Aug 9, 2018

    @ahejlsberg
    Member

    This is working as intended. A string index signature places a constraint on every property of a type, regardless of what kind of name it has. I agree the name "string index signature" is a bit unfortunate as it affects properties that might have numeric or symbol names, but the name predates our support for those kinds of names. It would definitely be a breaking change to modify our behavior here.

  5. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    BugA bug in TypeScript
    on Aug 9, 2018
  6. yortus commented on Aug 10, 2018

    @yortus
    ContributorAuthor

    Oh right. I just assumed the strings and symbols were separated out as part of #23592 (and the breaking change taken there).

    So what about this third variation, is it also working as intended?

    const SYM = Symbol();
    type StringProps3<T> = T extends {[K in string]: infer U} ? U : never;
    type T3 = StringProps3<{a: 'aaa', [SYM]: 'sym'}>;   // T2 = 'aaa' | 'sym'

    The K in that mapped type is also picking up the symbol key, so K in string is including non-string values.

  7. yortus commented on Aug 10, 2018

    @yortus
    ContributorAuthor

    Another thing I can't figure out is, if {[x: string]: T} is intended to apply to all keys (i.e., strings, symbols and numbers), then why is the following not an error?

    const SYM = Symbol();
    declare function foo(obj: {[x: string]: number}): void;
    foo({a: 1, [SYM]: 'sym'});  // OK

    In this case, the symbol key appears not to be part of the 'string index signature' since it is not constrained to number.

    From my perspective, this behaviour seems correct (ie symbols are excluded from the string index signature), and the prior 'working as intended' behaviour seems incorrect. But either way, shouldn't the inferring and the constraining behaviour both be consistent w.r.t. including symbols in the string index signature?

  8. yortus commented on Aug 10, 2018

    @yortus
    ContributorAuthor

    For context, the following code is a cut-down repro of what I'm trying to do, but I can't figure out what's going on with the compiler. Playground link.

    The intent here is that all three cases should work without errors. According to Anders Hejlsberg (@ahejlsberg)'s comment above I suspect all three should be errors. In reality 2/3 work. In the end, is there a way to express the intended inference logic of the foo function?

    // function foo takes an 'options' object that can have:
    // (a) any number of string keys whose values are unary functions
    // (b) an optional 'directive' unique symbol key with a unrelated type
    const directive = Symbol('directive');
    declare function foo<TArg, TRet, TDir>(options:
        {[x in string]: (arg: TArg) => TRet}    // all the string keys are unary functions
        & {[directive]?: TDir}                  // there is an optional 'directive' symbol key
    ): void;
    
    // CASE 1
    // ~~~ERROR~~~ - ... prop 'addOne' incompatible with index signature
    //               ... 'x' and 'arg' are incompatible
    //               ... 'string' not assignable to 'number'
    let case1 = foo({
        [directive]: (x: string) => 'str',
        addOne: (x: number) => x + 1,
        double: (x: number) => x + x,
    });
    
    // CASE 2: identical to case 1 except for order of properties
    // Compiles OK - infers TArg = number, TRet = number, TDir = (x: string) => string
    let case2 = foo({
        addOne: (x: number) => x + 1,
        double: (x: number) => x + x,
        [directive]: (x: string) => 'str',
    });
    
    
    // CASE 3: identical to case 1 except for type of directive
    // Compiles OK - infers TArg = number, TRet = number, TDir = string
    let case3 = foo({
        [directive]: 'str',
        addOne: (x: number) => x + 1,
        double: (x: number) => x + x,
    });
  9. yortus commented on Aug 15, 2018

    @yortus
    ContributorAuthor

    Closing since it's working as intended. I opened a separate issue (#26470) for the code in the preceding comment.

  10. 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

    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