★ wanayoo — archive 1999 https://github.com/microsoft/TypeScript/issues/2049Nouvelle recherche | Portail wanayoo
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

General indexer type #2049

Open
NN--- opened this issue Feb 16, 2015 · 13 comments

Comments

@NN---
Copy link

commented Feb 16, 2015

It is impossible to write generic type that represents key-value dictionary:

interface KeyValue<Key, Value> {
    [key: Key]: Value;
}

Key must be string or number, but generic constraints doesn't allow this.

interface KeyValue<Key extends string|number, Value> {
    [key: Key]: Value; // Still error
}

There is 'extends' but there is no 'is' to match exact type.
Something like "Key is string|number" or "Key: string|number" .

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Feb 21, 2015

So for your example, I think what you're looking for is really the ability to index with a union type:

interface Map<T> {
    [key: string | number]: T;
}

Which wasn't allowed when we first introduced union types, but for 1.5 you should have it through #1765.

@danquirk danquirk reopened this Apr 3, 2015

@danquirk

This comment has been minimized.

Copy link
Member

commented Apr 3, 2015

This wasn't a change we made, so re-opening this although it's not clear how useful it is.

@yjo

This comment has been minimized.

Copy link

commented Aug 6, 2015

+1, am wanting to do this now to write a type definition for Lodash's _.mapKeys

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

commented Jan 4, 2016

This needs more fleshing out. Currently number and string indexers have very different behavior; it's not at all obvious what the behavior of the "combined" key type would be. At a minimum we need to see what the desired behavior of this would be other than simply allowing its declaration to exist.

@aindlq

This comment has been minimized.

Copy link

commented Aug 3, 2016

At least it would be very useful to have the ability to use string literal union type as an indexer. As people have already pointed out in #5185 (comment)

It is more limited than initial proposal, but added value will be tremendous.

@danielearwicker

This comment has been minimized.

Copy link

commented Sep 2, 2016

And now number literal union type as indexer!

@jovdb

This comment has been minimized.

Copy link

commented Aug 4, 2017

I also would like to have indexer with generics support:

interface IPerson<TPersonId extends string> {
    readonly id: TPersonId;
    fullName: string;
}

interface IPersons {
    <TKey extends string>[key: TKey]: IPerson<TKey>;   // Made-up syntax
}

to represent:

const persons: IPersons = {
    "a14578": {
        id: "a14578",
        fullName: "Jo Van den Berghe"
    } 
}
@weswigham

This comment has been minimized.

Copy link
Member

commented Aug 25, 2017

The examples in this thread can be represented, today, with mapped types.

type KeyValue<Key extends string, Value> = {[K in Key]: Value};

interface IPerson<TPersonId extends string> {
    readonly id: TPersonId;
    fullName: string;
}
type IPersons<TKey extends string> = {[K in TKey]: IPerson<K>};

Is there still a feature request here?

@c69

This comment has been minimized.

Copy link

commented Mar 14, 2018

The problem with mapped types is that once you converted anything to type, you cannot use it as an interface, e.g.: no more implements for classes, and that's quite a bit trade-off.

I wonder if there are any ideological objections to allowing simple mapping syntax inside of interfaces.

type AllWorks<T> = {
    [K in keyof T]: T[K]; // ok
}
interface DoesNotWork<T> {
    [K in keyof T]: T[K]; // would be great for this syntax to work inside of interfaces
}

While first one works as expected, second one gives the TS error:

[ts] A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
[ts] Member '[K in keyof' implicitly has an 'any' type.
[ts] Cannot find name 'keyof'.

@kevupton

This comment has been minimized.

Copy link

commented Apr 17, 2018

I think what would be ideal is having generic types, but having the compiler validate the type when it is used. (for the key validation).

An example where a function returns:

{ [key : T[K]] : T }

T being an object, and K being keyof T

Example Code:

A function which takes an array of objects, and returns an object map, with the each object mapped to the input key.

interface Type {
  [key: string]: any;
}

/**
 * Function which maps an array to an object.
 * This will convert [{id, test}...] => {id: {id, test}...}
 * Useful for creating cache maps.
 *
 * @param {K} key
 * @param {T[]} objects
 * @returns {{[p: string]: T}}
 */
export function mapArrayToObject<T extends Type, K extends keyof T>(key: K, objects: T[]): {
  [key: T[K]]: T; // it should recognise that this is a variable type which depends on the input, and validate it on use.
} {
  return Object.assign(
    {},
    ...objects.map(object => ({ [object[key]]: object }))
  );
}

Then when they use the method:

This should work

interface Test {
  id: string;
  value: any;
}
const testArray: Test[] = [];
mapArrayToObject('id', testArray); // this should work because id is a string type and key accepts string.

This should fail

interface Test2 {
  id: object;
  value: any;
}
const test2Array: Test2[] = [];
mapArrayToObject('id', test2Array); // this should fail because it recognises that `id` is an object. And you cannot have an `object` as a key.
@felixfbecker

This comment has been minimized.

Copy link

commented May 11, 2018

@mhegazy Is there an issue tracking the is contraint suggestion in the OP? I am trying to write a propertyIsDefined function like this to help with #10976 (comment):

export const isDefined = <T>(val: T): val is NonNullable<T> => val !== undefined && val !== null

export const propertyIsDefined = <T extends object, K extends keyof T>(key: K) =>
  (val: T): val is T & { [k in K]: NonNullable<T[k]> } => isDefined(val[key])

which works, but it is only type safe if K is a single string literal. If K is a union, this function is not type safe anymore, as it would mark all properties in K as non-null, instead of one of the properties in K. I would like to forbid passing in a union, K should be an exact element of keyof T, but extends allows to pass any subtype of keyof T.

@weswigham

This comment has been minimized.

Copy link
Member

commented May 12, 2018

@felixbecker you just wanna decompose any input unions before you map, eg

export const propertyIsDefined = <T extends object, K extends keyof T>(key: K) =>
  (val: T): val is (K extends any ? T & { [k in K]: NonNullable<T[k]> } : never) => isDefined(val[key])

which should handle the input as union case like you'd like.

@felixfbecker

This comment has been minimized.

Copy link

commented May 12, 2018

@weswigham wow, works like a charm! I would have never thought about that. How does it work? Shouldn't K extends any always be true?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.