microsoft / TypeScript

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

Regression: Type T[K] as an array when sliced loses its type #31549

Open e-davidson opened 5 years ago

e-davidson commented 5 years ago

TypeScript Version: 3.4.5

Search Terms: array property generics Code

export function GetArrayProp<U, K extends string, T extends Record<K, U[]>>(
  state: T,
  prop: K,
) {
  //good
  let x1: T[K] = state[prop];
  //good
  let x2: U[] = state[prop];
  //good
  let x3: U[] = state[prop].slice(0);
  //error
  let x4: T[K] = state[prop].slice(0);
}

While you can argue that U[] isn't necessarily assignable to type T[K]. Look at the following code. It's hard to see how TypeFromArray<T[K]>[]'s type should be any different than T[K]

export type TypeFromArray<T> = T extends (infer R)[] ? R : never;

export function GetArrayProp<K extends string, T extends Record<K, Array<TypeFromArray<T[K]>>>>(
  obj: T,
  prop: K,
) {
  //good
  let x1: T[K] = obj[prop];
  //good
  let x2 = obj[prop].slice(0);
  //error Type 'TypeFromArray<T[K]>[]' is not assignable to type 'T[K]'.
  let x3: T[K] = obj[prop].slice(0);
}

Expected behavior:

No errors. T[K] and U[] should be assignable to each other. state[prop].slice(0) should be of the same type as state[prop] TypeFromArray<T[K]>[]'s type should be not be any different than T[K]

This code works in version 3.2.4 but stopped working in 3.3.1

Actual behavior:

Error Type 'U[]' is not assignable to type 'T[K]'. Error Type 'TypeFromArray<T[K]>[]' is not assignable to type 'T[K]'

Playground Link:

Playground Link

My main goal is to be able to pluck array properties and modify the arrays without losing the type T[K] The code above is simplified to show how TS is behaving. See my use case below.

It's hard to see how TypeFromArray<T[K]>[]'s type should be any different than T[K] The code below used to work when using 3.2.4


import { EntityState, EntityAdapter } from '@ngrx/entity';
import { Action } from '@ngrx/store';

export type TypeFromArray<T> = T extends (infer R)[] ? R : never;

type Selector<T> = (s: T) => boolean;

function ModifyEntityFromState<T>(entityId: number, state: EntityState<T>, callBack:  (acnt: T) => void): T {
  const t = state.entities[entityId];
  const acnt = JSON.parse(JSON.stringify(t)) as T;
  callBack(acnt);
  return acnt;
}

export function ModifyPropertyFromState<T, K extends keyof T>(
  entityId: number,
  state: EntityState<T>,
  prop: K,
  callBack: (a: T[K]) => T[K],
): T {
  return ModifyEntityFromState( entityId, state, (acnt) => {
    acnt[prop] = callBack( acnt[prop]  );
  });
}

export function EditArrayFromState<
T extends Record<K, Array<TypeFromArray<T[K]>>>,
K extends keyof T,
U extends TypeFromArray<T[K]>
>(
  entityId: number, state: EntityState<T>, prop: K,
  selector: Selector<U>,
  modified: U
): T {
  return ModifyPropertyFromState( entityId, state, prop, (a) => {
//Argument of type '(a: T[K]) => TypeFromArray<T[K]>[]' is not assignable to parameter of type '(a: T[K]) => T[K]'.
//  Type 'TypeFromArray<T[K]>[]' is not assignable to type 'T[K]'
    const idx = a.findIndex(selector);
    return  [
      ...a.slice(0, idx),
      ...a.slice(idx + 1),
      modified
    ];
  });
}
RyanCavanaugh commented 5 years ago

Changed between 3.2 / 3.3

=== 3.4 ===

clipboard.ts:7     const k: T[K] = j.slice();
                         ~
  TS2322: Type 'U[]' is not assignable to type 'T[K]'.
=== 3.3 ===

clipboard.ts:7     const k: T[K] = j.slice();
                         ~
  TS2322: Type 'U[]' is not assignable to type 'T[K]'.
=== 3.2 ===
=== 3.1 ===
ypresto commented 5 years ago

Another case of T[K] type loss at slice() call on generics array:

interface Table {
  foo: string
  bar: string | number
  baz: number
}

type ArrayTable = { [K in keyof Table]: Table[K][] }

function get<K extends keyof Table>(key: K): Table[K] {
  const arrayTable = {} as ArrayTable
  const array = arrayTable[key] // MapTable[K]
  const sliced = array.slice() // (string | number)[] , expected K[]
  // Type '(string | number)[]' is not assignable to type 'Table[K]'.
  //   Type '(string | number)[]' is not assignable to type 'never'.
  return sliced
}

TypeScript Version: 3.5.2 But this also reproduces in 3.1.6, maybe similar but different issue..?