microsoft / TypeScript

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

Boost type inference capability (keyof / generics / object mapping) #15415

Closed arogg closed 4 years ago

arogg commented 7 years ago

TypeScript Version: 2.2.2

Code Using noImplicitAny,strictNullChecks, target: es2017, lib: es2015, dom

function mapValues<T, K extends keyof T, R>(t: T, func: (value: T[K], key: K) => R): {[K2 in K]: R} {
  throw new Error('Not implemented.');
}

const vals = {
  value1: { type1: true },
  value2: { type2: false }
}
const val = mapValues(vals, (v, k) => Observable.of(v));

Expected behavior:

I expected val to have type (ok, more like hoping :)

type expectedTypeOfVal = {
  value1: Observable<{ type1: true }>,
  value2: Observable<{ type2: false }>
}

Actual behavior:

But instead it has this type

type actualTypeOfVal = {
  value1: Observable<{
    type1: boolean;
  } | {
    type2: boolean;
  }>;
  value2: Observable<{
    type1: boolean;
  } | {
    type2: boolean;
  }>;
};

This means I cannot really do anything meaningful with these observables..., because the union types mean i cannot use type1 or type2properties without writing more boilerplate code.

So i always have to manually cast the object to: (which is boilerplate code by itsself)

type expectedTypeOfVal = {
  value1: Observable<{ type1: true }>,
  value2: Observable<{ type2: false }>
}
// or
type expectedTypeOfVal = {
  [K in keyof typeof vals]: Observable<typeof vals[K]>;
}

So there are 2 questions i have:

a) have I overlooked something when writing the mapValues()function that could help me in this case?

b) if NOT: is there any chance the TypeScript team could look into boosting the capability of type inference for these cases (or maybe boost generic typing capability for these cases)?

With all the dataprocessing going on in lots of apps, it really would help a LOT (either reducing boilerplate or improving type safety for the "lazy" 'any' users).

PS. I know libs like lodash have lots of other issues, so I am writing my own typings for lodash, which is proving to be far better :) (levering keyof, declarative interfaces etc :) But unfortunately this problem remains :)

mhegazy commented 7 years ago

The type of vals is inferred to be const { value1: { type1: boolean }, value2: { type2: boolean} }, the reason is literal types (true and false in this case) are always widened to their base types (boolean) if they are in a mutable location, e.g. non-readonly property assignment.

the rational here is it is strange for a non-readonly property to have only one value.

in the object literal case, it is not clear if you intend for this to be used as a constant, that will not change, and hence the compiler can safely give it a restricted literal type, or it is meant as an initialization that you will change in the future. We have discussed in the past some syntax to denote an object literal property as readonly.

RyanCavanaugh commented 4 years ago

as const is now available as mentioned