Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using 'Key in keyof ArrayType' always returns an Array regardles of the value manipulation applied #40069

Open
KilianKilmister opened this issue Aug 15, 2020 · 4 comments
Labels
Needs Investigation This issue needs a team member to investigate its status.
Milestone

Comments

@KilianKilmister
Copy link

TypeScript Version: 4.0.0-beta (same behaviour in version 3.5.1 v3.5.1-Play Ground)

Search Terms:
"key in keyof Array", "keyof Array"

Expected behavior:
using key in ArrayType has the same behaviour as on any object (returning an array type only if the relevant properties are unchanged)

Actual behavior:
using key in ArrayType performs actions on touple entries but the returned type allways includes all default Array properties

Extention note:
Not the point of this issue and i know there are other issues around this topic. it's just a note,
but how about possibly adding tupple functionality for ArrayLike-, Map- and Set-types? They have all the basic qualities of a tupple, and it would be awesome to use them in a similar fashion (especially inference of Map.prototype.get/set and [Symbol.iterator] inference of tupples)

Related Issues:
#39726
#34780
type-fest/issues/119 (external library, but about this this behaviour and includes adHoc fix)

Code

type Arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
type Arr2 = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[]
type WhoopMyArray_Faulty<Base extends ArrayLike<any>, Condition> = {  
		[Key in keyof Base]:
			Base[Key] extends Condition
				? 'Whoop it'
				: Base[Key] extends Base[number] ? Base[Key] : never
	}

// action is performed on array elements but return type is still a proper array
type WhoopedArray = WhoopMyArray_Faulty<Arr, 7 | 5 | 2>
type WhoopedArrayKeys = WhoopMyArray_Faulty<Arr, 7 | 5 | 2>[keyof Arr]

// related side note, but not the main pont
// no action performed. in line with 1st example, expected: (0 | 1 | 'Whoop it' | 3 | 4 | 'Whoop it' | 6 | 'Whoop it' | 8 | 9)[]
type WhoopedArray2 = WhoopMyArray_Faulty<Arr2, 7 | 5 | 2>
type WhoopedArray2Keys = WhoopMyArray_Faulty<Arr, 7 | 5 | 2>[keyof Arr]

infered types:

type WhoopedArray = [0, 1, "Whoop it", 3, 4, "Whoop it", 6, "Whoop it", 8, 9]

type WhoopedArrayKeys = 0 | "Whoop it" | 1 | 3 | 4 | 6 | 8 | 9 | 10 | (() => string) | (() => string) 
  | (() => 0 | "Whoop it" | 1 | 3 | 4 | 6 | 8 | 9 | undefined) | ((...items: (0 | "Whoop it" | 1 | 3 | 4 | 6 | 8 | 9)[]) => number)
  | {    ...;    } | ... 22 more ... | (() => IterableIterator<...>)

type WhoopedArray2 = (0 | 7 | 5 | 2 | 1 | 3 | 4 | 6 | 8 | 9)[]

type WhoopedArray2Keys = 0 | "Whoop it" | 1 | 3 | 4 | 6 | 8 | 9 | 10 | (() => string) | (() => string)
  | (() => 0 | "Whoop it" | 1 | 3 | 4 | 6 | 8 | 9 | undefined) | ((...items: (0 | "Whoop it" | 1 | 3 | 4 | 6 | 8 | 9)[]) => number)
| {     ...;    } | ... 22 more ... | (() => IterableIterator<...>)
Output
"use strict";
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": true,
    "alwaysStrict": true,
    "allowUnusedLabels": true,
    "noEmitHelpers": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": 2,
    "target": "Latest",
    "module": "ESNext"
  }
}

Playground Link: Provided

@jcalz
Copy link
Contributor

jcalz commented Aug 16, 2020

This working as intended. Mapped arrays/tuples stay arrays/tuples as of TypeScript 3.1. See #26063.

@KilianKilmister
Copy link
Author

I would argue that mapping its method & length keys to never makes the resulting object non-homomorphic. It also causes generics that accept object-types act unpredictable and in no way type-safe.

While it does work in accordance of the mentiond PR, this should then be considered a design oversight.

ForReference: Playground Link

type Values<T> = T[keyof T]
type ReduceToUnique<T> = { [P in keyof T]: T[P] extends typeof Unique ? 'this was unique' : never }
const Unique = Symbol("i'm unique")


// 6 different Types, all of them extend `object`
declare const objectType: object
declare const anyType: any
declare const notUniqueable: {someKey: 'someValue'}
declare const isUniqueable: {someKey: typeof Unique}
declare const notUniqueableArray: [1, 2, 3, 4, 5, 6, 7]
declare const isUniqueableArray: [1, 2, 3, 4, 5, 6, typeof Unique]

// a function that takes a generic `T extends object`
declare function reduceToUniqueKV <T extends object>(someObject: T): ReduceToUnique<T>

// the 6 Types processed by that function
const objectT = reduceToUniqueKV(objectType)
// ^-> object
const anyT = reduceToUniqueKV(anyType)
// ^-> ReduceToUnique<any>
const notU = reduceToUniqueKV(notUniqueable)
// ^-> ReduceToUnique<{ someKey: 'someValue'; }>
const isU = reduceToUniqueKV(isUniqueable)
// ^-> ReduceToUnique<{ someKey: typeof Unique; }>
const notUArr = reduceToUniqueKV(notUniqueableArray)
// ^-> [never, never, never, never, never, never, never]
const isUArr= reduceToUniqueKV(isUniqueableArray)
// ^-> [never, never, never, never, never, never, "this was unique"]

// reducing the results to their property values
declare const objectTValues: Values<typeof objectT> 
// ^-> never
declare const anyTValues: Values<typeof anyT> 
// ^-> 'this was unique'
declare const notUValues: Values<typeof notU> 
// ^-> never
declare const isUValues: Values<typeof isU>
// ^-> 'this was unique'
declare const notUArrValues: Values<typeof notUArr>
// ^-> 7 | (() => string) | (() => string) | (() => undefined)
// | ((...items: never[]) => number) 
// | { (...items: ConcatArray<never>[]): never[];
//   (...items: ConcatArray<never>[]): never[]; } 
// | ... 22 more ... | { ...; }
declare const isUArrValues: Values<typeof isUArr>
// ^-> 7 | "this was unique" | (() => string) | (() => string) 
// | (() => "this was unique" | undefined)
// | ((...items: "this was unique"[]) => number)
// | { (...items: ConcatArray<"this was unique">[]): "this was unique"[]; 
//   (...items: ("this was unique" | ConcatArray<...>)[]): "this was unique"[]; } 
//  | ... 22 more ... | { ...; }

@yume-chan
Copy link
Contributor

A mapped type will not touch length and methods keys for an array type, for example

type ToNevers<T> = { [K in keyof T]: never };
type A = ToNevers<string[]>['length']; // still number, not never

The problem is you can't do Arr[keyof Arr] to get its "values", you can use Arr[number], so your Values should be

type Values<T> = T extends unknown[] ? T[number] : T[keyof T];

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Sep 3, 2020
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Sep 3, 2020
@KilianKilmister
Copy link
Author

@yume-chan completly missed this response, so my apologies for the late reply

The problem is you can't do Arr[keyof Arr] to get its "values", you can use Arr[number], so your Values should be

type Values<T> = T extends unknown[] ? T[number] : T[keyof T];

Sure, this is easy when you expect an array, but if you just expect any generic object, this will really suprise you, see the example in my comment. Arrays have to be explicitly filtered out and handled differently (which i started doing after i ran into this).

But this also has become much less of an issue with some of the awedome new features #40336

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

4 participants