1

Let's say we have a type T:

type T = {
  type: string,
}

and a function that accepts an array of T and returns an object whose keys are the values of every T.type and whose values are T

const toMap = (...args: T[]) => args.reduce((res, t) => ({
  ...res,
  [t.type]: t
}), {});

so, for this given example:

const a = { type: 'hello' };
const b = { type: 'world' };
const c = { type: 'foo' };

const map = toMap(a, b, c);

I expect this result

{
  hello: { type: 'hello' },
  world: { type: 'world' },
  foo: { type: 'foo' },
}

map.hello // correct, { type: 'hello' };

// If I access an unknown property, then the compiler should: 
map.bar // `property bar doesn't exist on type { hello: { ... }, world: {...}, foo: {...} }`

how can I write typings for this function?

1 Answer 1

2

You could start by making T really generic:

function toMap<T extends { type: string }>(...args: T[]): { [type: string]: T } {
  return args.reduce((res, t) => ({
    ...res,
   [t.type]: t
  }), {});
}

To be then able to really narrow down the types, you have to type generic types for variable arguments, e.g. toMap<A>(arg1: A), toMap<A, B>(arg1: A, arg2: B).

There are two downsides though:

1) You have to create these overloads for any number of arguments, however that is common in Typescript (see Object.assign declaration).

2) Typescript types { type: "test" } as { type: string } by default (which is wanted in 99% of the cases), however therefore we can't infer the keys type to "test" directly. To solve this, we have to typecast the string literal to a narrowed down string type { type: "test" as "test" }.

// generic overload for one argument
function toMap<A>(arg: A): { [K1 in O<A>]: A };

// generic overload for two arguments:
function toMap<A, B>(arg: A, arg2: B): { [K in O<A>]: A } & { [K in O<B>]: B };

// generic overload for three arguments:
function toMap<A, B, C>(arg: A, arg2: B, arg3: C): { [K in O<A>]: A } & { [K in O<B>]: B } & { [K in O<C>]: C };

// ... repeat for more arguments

// implementation for all kind of args
function toMap<T extends { type: string }>(...args: T[]): { [type: string]: T } {
   return args.reduce((res, t) => ({
     ...res,
    [t.type]: t
  }), {});
}

// Infers the type of "type", which has to be a string, from a given object
type O<V> = V extends { type: infer K } ? K extends string ? K : never : never;

// Narrow down a.type to be "test" instead of string
const a = { type: "test" as "test" }
const b = { type: "test2" as "test2", v: 1 };

const test = toMap(a);
const test2 = toMap(a, b);

console.log(
 test2.test2.v, // works!
 test2.whatever, // doesnt!
 test2.test2.k // doesnt!
);

Try it!

Sign up to request clarification or add additional context in comments.

10 Comments

I don't think this really works, keys should be the value of T.type
I think the way to go would be keyOf
that is great, but on my real case I can't do the type assertion as <literal> and it doesn't work. (due to the initial overload, it also errors if you pass more than 1 argument)
@htimands if you can't do the type assertion, your code isn't static at compile time, and therefore you can't narrow down the types
here is the package I am working on github.com/Code-Y/redux-fluent
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.