Skip to content

string not assignable to Record with specific number key #59489

Closed as not planned

Description

🔎 Search Terms

string not assignable record key number object

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about "Here are some behaviors that may look like bugs, but aren't."

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.5.4#code/FAUwHgDg9gTgLgAgGYFcB2BjOBLKaEDmIcA0iAJ4A8Z5C4cIaAJgM4IDWFUSCAhmuQA0CAGq8ANihAA+ABTAAkADcJUgFwIASiAywm1CsLGSZgxZ3IaaZgJQbjUhAG9FMYihj4VJgNoWAugDcwAC+wAD04XQwMLDARKQUsgBEAIwATADMycIADDbBEVFQ7PHENCkZ2Xl8bGgoALYARiAwBWWJ5LI+qcLpwpn+ee1AA

💻 Code

export function getKey<Key extends keyof any, Value>(
	value: Record<Key, Value>,
	key: Key,
): Value {
	return value[key];
}
// error
getKey("123", 0);

// ok
getKey("123", 0 as number);
getKey([1, 2, 3], 0);

🙁 Actual behavior

An error occurs because a string is not assignable to a record type with an index type that is a specific number. This seems wrong because string are generally indexable by numbers. And array types are indexable by specific number types.

🙂 Expected behavior

No error when trying to assign a string to a record type where the index is a specific number.

Additional information about the issue

I bumped into this when trying to implement a generic comparator function like compareBy(0)('abc', '123') === 1.

Activity

  1. RyanCavanaugh commented on Jul 31, 2024

    @RyanCavanaugh
    Member

    This sort of falls out of the inference rules and it's not super clear which rule would need to change here that wouldn't break something else much more badly.

    Since this can be written more straightforwardly this way, I wouldn't really consider this a bug

    export function getKey<V, K extends keyof V>(value: V, key: K): V[K] {
    	return value[key];
    }
  2. camsteffen commented on Jul 31, 2024

    @camsteffen
    Author

    Here's another example, closer to what I was trying to do:

    type Comparator<T> = (a: T, b: T) => number;
    
    function compareBy<K extends keyof any>(
    	key: K,
    ): Comparator<Record<K, unknown>> {
    	return (a, b) => {
    		// TODO
    	};
    }
    
    function test(list: string[]) {
    	// error
    	list.sort(compareBy(0));
    	// ok
    	list.sort(compareBy(0 as number));
    }
  3. RyanCavanaugh commented on Jul 31, 2024

    @RyanCavanaugh
    Member

    It's a little awkward to try to figure out the intended semantics here. It seems like you'd want compareBy("x") to only accept things with a declared { x: something }, but then for 0, strings don't have a declared 0 property (and indeed might be zero-length), so rejecting "" because it doesn't have { 0: something } is just as correct as the rejection of "x".

  4. camsteffen commented on Jul 31, 2024

    @camsteffen
    Author

    It seems like you'd want compareBy("x") to only accept things with a declared { x: something },

    compareBy(0) could also be used for comparing lists by the first element. I admit indexing strings is a little edgy, but I think there is value in having this work generically.

    so rejecting "" because it doesn't have { 0: something } is just as correct as the rejection of "x"

    But the error occurs even if you change the string type to a specific non-zero-length string.

    Also I think we should expect strings and arrays to be consistent in this regard.

    function hasZeroKey(x: Record<0, any>) {}
    function hasNumberKey(x: Record<number, any>) {}
    hasZeroKey("abc"); // error
    hasNumberKey("abc"); // ok
    hasZeroKey([1, 2, 3]); // ok
    hasNumberKey([1, 2, 3]); // ok
  5. RyanCavanaugh commented on Aug 1, 2024

    @RyanCavanaugh
    Member

    It is consistent, it's just that strings don't get any contextual tuple-like inference the way arrays do (it's unclear where that would ever be useful):

    const arr = [1, 2, 3];
    hasZeroKey(arr); // Error
  6. camsteffen commented on Aug 2, 2024

    @camsteffen
    Author

    There doesn't need to be tuple-like inference. Typescript optimistically assumes that indexing an unknown-length array will yield an element. It should also assume that indexing a string yields a string. In fact string[number] or string[0] does equal string, but it just doesn't allow you to express that rule in terms of a record type. But you can with arrays. So that seems definitely inconsistent to me.

  7. camsteffen commented on Aug 2, 2024

    @camsteffen
    Author

    I found a workaround for my compareBy use case.

    type Comparator<T> = (a: T, b: T) => number;
    
    type CompareBy<K extends keyof any> = Comparator<
    	| Record<K, unknown>
            // explicitly allow string here
    	| (K extends number ? string : never)
    >
    
    function compareBy<K extends keyof any>(key: K): CompareBy<K> {
    	return (a, b) => {
    		if (a[k] < b[k]) return -1;
    		if (a[k] < b[k]) return 1;
    		return 0;
    	};
    }
    
    function test(list: string[]) {
    	// ok - fixed
    	list.sort(compareBy(0));
    }
  8. RyanCavanaugh commented on Aug 2, 2024

    @RyanCavanaugh
    Member

    The thing that allows this

    function hasZeroKey(x: Record<0, any>) {}
    hasZeroKey([1, 2, 3]); // ok

    is that the [1, 2, 3] literal gets inferred to be a tuple (which has declared 0, 1, 2 properties). Without that tuple inference, it's just an object with a numeric index signature, and it's intentional that an index signature isn't allowed to satisfy a declared property.

  9. camsteffen commented on Aug 2, 2024

    @camsteffen
    Author

    Hmm I see. I thought I saw it working with number[] but it doesn't.

  10. typescript-bot commented on Aug 5, 2024

    @typescript-bot
    Contributor

    This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

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

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions