microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.84k stars 12.46k forks source link

Template Literal Types derived from the type keys cannot be used as Indexed Access Types in generic contexts #59909

Open SmolPatat opened 1 month ago

SmolPatat commented 1 month ago

🔎 Search Terms

template literal, indexed access, keyof, key of

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.1-rc#code/PTAEBUE8AcFMGdQHcCWAXAFqAdgVwLawBOKAxqANayTwB0AUGjLKAIKgC8oA2gIwC6AbkbNQAIU6he3ISLigAwpIDePKpABcOAgCNi-Lb1ABfYU3kARFaAAMhk8Pog2AGxcB7JLAAmoFNm9YAA8fUABDUlIERHMEBljXDy9fLm5Wbht+ABo2bgByGzzs3Lx8PSJi9IADABJlUvLjKuKxDJb8wvaG-RzW2vrdYibihTac0YKi8e5uiun+2eGcizHQFcn+WWcLFHgwt09Q-0CQ3wio+BjmOjkWHb2D5Mk07ng0EmwAc3a3j+-p37+f5rV7vIHFFazCHcBaDIjDWS3UAAOXcaFY2AAkpdcLAADzgUDBNCwAKIdw6ABWsFIaAAfJJwDC6up3AAzCDDMyibHwXEEolBElk0AU6m0hlcJn9VkcwkAMlAAApAV9QAAfbRlYga0A6FCffxoXU6dzuFywMLYXV4Ny63ABWBs-w+ACUXKRCnc+GgETQvP5hOJpO85KpNPpjOZygAokFSC5cIE8bKIDl4JAyua6VygA

💻 Code

// Types with numeric keys.
type A = [1];
type B = 1[];
type C = { [key: number]: 1 };
type D = { 0: 1 };

// Allowed indexed access types.
type Allowed = [A[0], A['0'], A[number], A[`${number}`], B[0], B['0'], B[number], B[`${number}`], C[0], C['0'], C[number], C[`${number}`], D[0], D['0']];
// Disallowed indexed access types.
type Disallowed = [A[string], B[string], C[string], D[string], D[number], D[`${number}`]];

type NotAnIssue<T extends object> = T[`${keyof T}`];
type Issue<T extends object> = T[`${keyof T & (string | number | bigint | boolean | null | undefined)}`];
type CompactIssue<T extends object> = T[`${Exclude<keyof T, symbol>}`];

🙁 Actual behavior

The template type literal generated from keyof T cannot be used as an indexed access type for T.

🙂 Expected behavior

The template type literal generated from keyof T or its restriction can be used as an indexed access type for T, since it is restricted to be a stringified version of the key type, which should behave exactly the same as the key type in this context.

Additional information about the issue

All object types with numeric keys can be accessed with the string version of that key. However, the compiler does not acknowledge this fact in the case of generics with proper restrictions on the type.

Using `${keyof T}` directly is prohibited because template literal types cannot be constructed from symbol types. However, if the type is restricted to the allowed string | number | bigint | boolean | null | undefined, TypeScript is still refusing to use keyof T for indexed access.

The issue does not happen with definite (non-generic) types and the issue also does not occur if `${Exclude<keyof T, symbol>}` is replaced with Exclude<keyof T, symbol> (which is still a problem for other use cases).

RyanCavanaugh commented 1 month ago

What's the use of being able to write this, if it were allowed?

SmolPatat commented 1 month ago

I came across this issue when writing stricter types for Object.keys(...) and Object.values(...). They are

type KeysOf<T extends object> = `${keyof T & (string | number)}`; // Simplified.

and

type ValuesOf<T extends object> = T[KeysOf<T>]; // Simplified.

respectively.

The issue is that KeysOf<T> cannot be used to index type T even though the type is guaranteed to contain only valid keys, which could be algorithmically deduced from the definition.

SmolPatat commented 1 month ago

The fundamental issue here is consistency. For any concrete type X, if another type Y can be used to index it as X[Y], then the expression X[`${Y}`] (if valid) always gives the same resulting type as X[Y].

This bug report / suggestion is about keeping this behaviour consistent when extending the same rule to generic types. Since there are no types that present an exception to the rule, it should work with generics as well.