Skip to content

Instantly share code, notes, and snippets.

@karol-majewski
Last active March 31, 2023 09:51
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karol-majewski/b234a4aceb8884ccc1acf25a2e1ed16e to your computer and use it in GitHub Desktop.
Save karol-majewski/b234a4aceb8884ccc1acf25a2e1ed16e to your computer and use it in GitHub Desktop.
Type inference for literal types with Object.fromEntries
type Primitive =
| boolean
| number
| string
| bigint
| symbol
| null
| undefined;
type Narrowable =
| Primitive
| object
| {};
type Entry<K extends PropertyKey, V> = [K, V];
/**
* @author https://stackoverflow.com/users/2887218/jcalz
* @see https://stackoverflow.com/a/50375286/10325032
*/
type UnionToIntersection<Union> =
(Union extends any
? (argument: Union) => void
: never
) extends (argument: infer Intersection) => void
? Intersection
: never;
type FromEntries<T extends Entry<K, V>, K extends PropertyKey, V extends Narrowable> =
UnionToIntersection<
T extends [infer Key, infer Value]
? Key extends PropertyKey
? Record<Key, Value>
: never
: never
>
function fromEntries<T extends Entry<K, V>, K extends PropertyKey, V extends Narrowable>(
entries: Iterable<T>,
): FromEntries<T, K, V> {
return [...entries].reduce(
(accumulator, [key, value]) =>
Object.assign(accumulator, {
[key.toString()]: value,
}),
{} as FromEntries<T, K, V>,
);
}
fromEntries([
['foo', 1],
['bar', 2]
]); // #ExpectType { foo: 1, bar: 2}
@hasparus
Copy link

hasparus commented May 17, 2019

Adding readonly pairs to Entry declaration

type Entry<K extends PropertyKey, V> = [K, V] | readonly [K, V]; // <-- here

is pretty helpful because as const is often a comfortable and concise way to get entries.

fromEntries(xs.map(x => [x.key, x] as const))

@karol-majewski
Copy link
Author

@hasparus good point! TypeScript seems to have trouble processing it though.

const xs = [{ key: 'foo' }, { key: 'bar' }] as const;
fromEntries(xs.map(x => [x.key, x] as const)) // #ExpectType { foo: { key: 'foo' }, bar: { key: 'bar' }}, got: unknown

I believe we would need the entire expression to be treated like a literal, not just the entry returned in the inner callback. And that's not possible since const contexts don't work for computed values. 😢

@hasparus
Copy link

If you add readonly here
image
it works (but the result is pretty mediocre)
image

@hasparus
Copy link

Actually, saying type Entry<K extends PropertyKey, V> = readonly [K, V]; makes sense then, because [K, V] is assignable to readonly [K, V]

@karol-majewski
Copy link
Author

Good observation. As to the results: I think you'd have to explicitly provide a type argument to xs.map<> in order to get the results we want. We would have to somehow map over the tuple and feed that type to .map.

type Entries<T> = {
  [K in keyof T]:
    T[K] extends Record<any, infer Value>
      ? Value extends PropertyKey
        ? [Value, T[K]]
        : never
      : never
};

type T1 = Entries<typeof xs>; // $ExpectType readonly [ ["foo", { readonly key: 'foo' }], ["bar", { readonly key: 'bar' }] ]

@karol-majewski
Copy link
Author

A similar case for UnionToIntersection for @maktarsis:

interface Path<T extends string> {
  path: T;
}

declare function fn<T extends Path<U>, U extends string>(paths: readonly T[]): Result<T>;

type Result<T extends Path<string>> =
  UnionToIntersection<
    T extends Path<infer Pathname>
      ? { [index in Pathname]: Pathname }
      : never
  >;

fn([{ path: 'home' }, { path: 'about' }]); // $ExpectType { home: 'home', about: 'about' }

const paths = [
  { path: 'home' },
  { path: 'about' },
] as const;

fn(paths); // $ExpectType { home: 'home', about: 'about' }

/**
 * @author https://stackoverflow.com/users/2887218/jcalz
 * @see https://stackoverflow.com/a/50375286/10325032
 */
type UnionToIntersection<Union> =
(Union extends any
    ? (argument: Union) => void
    : never
) extends (argument: infer Intersection) => void
    ? Intersection
    : never;

@karol-majewski
Copy link
Author

karol-majewski commented Jun 18, 2019

Use this definition to overload Object.fromEntries:

type Primitive =
  | boolean
  | number
  | string
  | bigint
  | symbol
  | null
  | undefined;

type Narrowable =
  | Primitive
  | object
  | {};

type Entry<K extends PropertyKey, V> = [K, V];

/**
 * @author https://stackoverflow.com/users/2887218/jcalz
 * @see https://stackoverflow.com/a/50375286/10325032
 */
type UnionToIntersection<Union> =
  (Union extends any
    ? (argument: Union) => void
    : never
  ) extends (argument: infer Intersection) => void
      ? Intersection
      : never;

type FromEntries<T extends Entry<K, V>, K extends PropertyKey, V extends Narrowable> =
  UnionToIntersection<
    T extends [infer Key, infer Value]
      ? Key extends PropertyKey
        ? { [Property in Key]: Value }
        : never
      : never
  >

declare global {
  interface ObjectConstructor {
    fromEntries<T extends Entry<K, V>, K extends PropertyKey, V extends Narrowable>(entries: Iterable<T>): FromEntries<T, K, V>
  }
}

Object.fromEntries([
  ['foo', 1],
  ['bar', 2]
]); // #ExpectType { foo: 1, bar: 2}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment