Skip to content

keyof for arrays #20965

Closed
Closed

Description

@bezreyhan

TypeScript Version: 2.5.3

Would it be possible to have something like the keyof operator for arrays? The operator would be able to access the values in the array.

Code
With object we can do the following:

const obj = {one: 1, two: 2}
type Prop = {
  [k in keyof typeof obj]?: string;
}
function test(p: Prop) {  
  return p;
}

test({three: '3'}) // throws an error

Could we do something like the following with arrays?

const arr = ['one', 'two'];
type Prop = {
    [k in valuesof arr]?: string;
  }
function test(p: Prop) {  
  return p;
}

test({three: '3'}) // throws an error

Activity

  1. DanielRosenwasser commented on Jan 2, 2018

    @DanielRosenwasser
    Member

    You can get valuesof arr with the indexed access type (typeof arr)[number], but you'll get string instead of "one" | "two" without an explicit type annotation. #10195 would help there though.

  2. bezreyhan commented on Jan 2, 2018

    @bezreyhan
    Author

    Thanks Daniel Rosenwasser (@DanielRosenwasser), #10195 would be helpful but ideally we would be able to do without indexed access types. What I was looking for was a way to express: "All keys in an object must be a value in an array".

  3. DanielRosenwasser commented on Jan 2, 2018

    @DanielRosenwasser
    Member

    You could always define a type alias for that, but you won't be able to get around using typeof in conjunction with it.

    export type ValuesOf<T extends any[]>= T[number];
    
    declare var x: ...;
    type Foo = ValuesOf<typeof x>;
  4. bezreyhan commented on Jan 2, 2018

    @bezreyhan
    Author

    That's interesting, I didn't think of that. I'll close out the ticket. Thanks!

  5. kpdonn commented on Mar 3, 2018

    @kpdonn
    Contributor

    bezreyhan I came across this issue while searching around for something else. I don't know if this will be useful to you two months later but I think this will do essentially what you want as long as you don't need to save the array in a variable beforehand:

    function functionGenerator<T extends string, U = { [K in T]?: string }> (keys: T[]): (p: U) => U {
      return (p: U) => p
    }
    
    const testFun = functionGenerator(['one', 'two'])
    
    testFun({one: '1'}) // no error
    testFun({two: '2'}) // no error
    testFun({three: '3'}) // error as expected because 'three' is not a known property

    If you do need the array in a variable, it looks like this will work:

    function literalArray<T extends string>(array: T[]): T[] {
        return array
    }
    
    const arr = literalArray(['one', 'two'])
    
    const testFun2 = functionGenerator(arr)
    testFun2({one: '1'}) // no error
    testFun2({two: '2'}) // no error
    testFun2({ three: '3' }) // error as expected because 'three' is not a known property

    Here is a link of those examples in the playground: Link

  6. tomzaku commented on Jun 15, 2018

    @tomzaku

    Kevin Donnelly (@kpdonn) Thanks. But I have problem with using array variable instead of direct array in parameter

    const functionGenerator = <T extends string, U = { [K in T]?: string }>(keys: T[]): U => {
      return keys.reduce((oldType: any, type) => ({ ...oldType, [type]: type }), {})
    }
    const data = ['one', 'two']
    const testFun = functionGenerator(data)
    testFun.one
    testFun.three <<<<<< also ok, expected throw error
  7. nickserv commented on Jun 16, 2018

    @nickserv
    Contributor

    Using resolveJsonModule, would it be possible to get the type of a JSON array without explicitly declaring the type of the JSON module and losing typechecking on its data? I tried the ValuesOf approach but it seems like I would have to declare the type of my function argument explicitly.

  8. tpucci commented on May 1, 2019

    @tpucci

    Kevin Donnelly (@kpdonn) I find your solution very interesting.
    Any idea how to have the same result with an array of object instead of an array of string ?
    The following code passes typescript diagnostic. However, the last line should throw an error.

    function functionGenerator<T extends {name: string}, U = { [K in T['name']]?: string }>(keys: T[]): (p: U) => U {
      return p => p;
    }
    
    const testFun = functionGenerator([{name: 'one'}, {name: 'two'}]);
    
    testFun({ one: '1', two: '2' }); // no error
    testFun({ two: '2' }); // no error
    testFun({ three: '3' }); // error expected but type diagnostic passes
  9. tpucci commented on May 2, 2019

    @tpucci

    Kevin Donnelly (@kpdonn) I find your solution very interesting.
    Any idea how to have the same result with an array of object instead of an array of string ?
    The following code passes typescript diagnostic. However, the last line should throw an error.

    If anyone is interested, I found the solution of my problem:

    function functionGenerator<
      V extends string,
      T extends {name: V},
      U = { [K in T['name']]?: string }
    >(keys: T[]): (p: U) => U {
      return p => p;
    }
    
    const testFun = functionGenerator([{name: 'one'}, {name: 'two'}]);
    
    testFun({ one: '1', two: '2' }); // no error
    testFun({ two: '2' }); // no error
    testFun({ three: '3' }); // throws an error
  10. NickDubelman commented on May 15, 2019

    @NickDubelman

    Is there anything wrong with doing the following:

    export type Things = ReadonlyArray<{
        readonly fieldA: number
        readonly fieldB: string
        readonly fieldC: string
        ...other fields
    }>
    
    type Thing = Things[number]
    

    When I do that, type Thing has the correct type. Obviously you wouldn't want to do this by default, but I find myself in the situation where I have something generating array types, but sometimes I want functions to take a single value of that array and obviously I don't want to have to manually duplicate the type definition.

    The above syntax is kind of weird, but it seems to do the job?

  11. benneq commented on Jun 17, 2019

    @benneq

    Typescript 3.4 made things better:

    const myArray = <const> ['foo', 'bar'];
    type MyArray = typeof myArray[number];
    

    👍

  12. Serrulien commented on Jul 10, 2019

    @Serrulien

    Ben M (@benneq) Thanks for that hint. I'm now able to type function's parameter without having to create an enum

    const pages = <const> [
        {
            label: 'homepage',
            url: ''
        },
        {
            label: 'team',
            url: ''
        }
    ];
    
    // resulting signature = function getUrl(label: "homepage" | "team"): void
    function getUrl(label: (typeof pages[number])['label']) {}
    
    getUrl('homepage') // ok
    getUrl('team') // ok
    getUrl('bad') // wrong
  13. bopfer commented on Jul 12, 2019

    @bopfer

    Is there a way to do what Serrulien posted, but also with types defined for each object in the array?

    Something like:

    type Page = {
      label: string;
      url: string;
    };

    Then enforce that type of each object of the const array. With an array of many elements, it would help to avoid type errors in any individual object.

  14. fabb commented on Jul 13, 2019

    @fabb

    You need to decide whether label should be of type string or only allow the constants defined in the array, 'homepage' | 'team'. The two contradict each other. Just safely typing the array without being able to extract a type for label values would work like this, but I‘m not sure if it suits your usecase:

    const pages: Page[] = [
        {
            label: 'homepage',
            url: ''
        },
        {
            label: 'team',
            url: ''
        }
    ]
    
  15. 11 remaining items

  16. Ash-oi commented on Jul 26, 2019

    @Ash-oi

    that is interesting, seems as though typescript isn't quite smart enough to be able inspect the arguments type?

    anyway thanks for your help Ben M (@benneq) and fabb

  17. Ash-oi commented on Jul 26, 2019

    @Ash-oi

    So I was playing around and I figured it out.

    function objectHasKeys<T extends string>(
      unknownObject: { [key: string]: unknown },
      requiredKeys: readonly T[]
    ): unknownObject is { [Key in T]: unknown } {
      return requiredKeys.every(
        required => unknownObject[required] !== undefined
      );
    }
    
    function test(thing: { [key: string]: unknown }) {
      const required = <const>['A', 'B']
      if (!objectHasKeys(thing, required)) return
      thing // <-- this is typed as { A: unknown, B: unknown }
    }
  18. daniel-nagy commented on Nov 29, 2019

    @daniel-nagy

    Is there a way to get a literal type from an iterative operation?

    function getValues<T>(object: T, keys: (keyof T)[]) {
      return keys.map((key) => object[key]);
    }
    
    const result = getValues({ "1": 1, "2": 2, "3": "three" }, ["1", "3"]);
    
    typeof result; // (string | number)[]

    I want the literal type of the result.

    typeof result; // [1, "three"]

    I'd also like to create a generic type to represent this function

    type GetValues<T, K extends readonly (keyof T)[]> = {
      (object: T, keys: K): T[K[number]][];
    };
    
    // produces (1 | "three")[], instead of [1, "three"]
    type Values = GetValues<{ "1": 1, "2": 2, "3": "three" }, ["1", "3"]>;
  19. AmirTugi commented on Jan 1, 2020

    @AmirTugi

    This is the release note with the relevant info about the solution (which Ben M (@benneq) mentioned)

  20. daniel-nagy commented on Jan 1, 2020

    @daniel-nagy

    Using const does not solve the problem for me.

    function getValues<T, K extends readonly (keyof T)[]>(
      object: T,
      keys: K
    ): T[K[number]][] {
      return keys.map(key => object[key]);
    }
    
    const result = getValues(
      { "1": 1, "2": 2, "3": "three" } as const,
      ["1", "3"] as const
    );
    
    typeof result; // produces (1 | "three")[], instead of [1, "three"]

    I'm trying to get a literal type from an operation on a literal type. I think the issue is K[number] will return a union type which has no understanding of order.

  21. fabb commented on Jan 1, 2020

    @fabb

    Daniel Nagy (@daniel-nagy) T[K[number]][] results in an array, but you want a tuple. You can keep the tuple type when you take advantage of mapped type support used like this: { [P in keyof T]: X } where T is the tuple. As far as I have understood, the keyof T is the tuple array index (since arrays in JS are just objects with indexes as keys), and this syntax builds a new tuple with the same indexes.

    All together this nearly works as expected:

    function getValues<T, K extends readonly (keyof T)[]>(object: T, keys: K): { [P in keyof K]: T[K[P]] } {
        return keys.map(key => object[key]) as any
    }
    

    While the result type is now correctly [1, "three"], TypeScript shows an error: Type 'K[P]' cannot be used to index type 'T'.. I think this is a bug in TypeScript, maybe related to #21760 or #27413.

    Here's a small workaround that fixes this error 🎉:

    function getValues<T, K extends readonly (keyof T)[]>(object: T, keys: K): { [P in keyof K]: T[K[P] & keyof T] } {
        return keys.map(key => object[key]) as any
    }
    
  22. 7kms commented on Jul 22, 2020

    @7kms

    in the latest ts "version": "3.9.6"

    export const extraKeys = ['app', 'title', 'package', 'deeplink', 'url', 'logo', 'image', 'type'] as const;
    
    export type Extra ={
      [key in typeof extraKeys[number]]: string;
    };
  23. Sharcoux commented on Jul 30, 2020

    @Sharcoux

    For a value, I found that I can use:

    const keys = ['app', 'title', 'package', 'deeplink', 'url', 'logo', 'image', 'type'] as const;
    type Key = typeof keys[0]
  24. derekslarson commented on Oct 25, 2020

    @derekslarson

    For what I am doing, something like this is sufficient:

    type Prop<AcceptableKeys extends Array<string>> = {
      [key in AcceptableKeys[number]]: string;
    };
    
    function test(prop: Prop<["one", "two"]>) {
      return prop;
    }
    
    test({ three: "3" }); // throws an error

    I specifically need this functionality for generating specific APIGatewayProxyEventV2 types for lambda integrations:

    type Request<
      PathParameters extends Array<string>,
      QueryParameters extends Array<string>,
      Body extends Record<string, unknown>
    > = {
      pathParameters: {
        [key in PathParameters[number]]: string;
      },
      queryStringParameters: {
        [key in QueryParameters[number]]: string;
      };
      body: Body;
    };
    
    type TestBody = {
      c: string;
    };
    
    type TestRequest = Request<["a"], ["b"], TestBody>;
    
    const testOne: TestRequest = {
      pathParameters: { a: "foo" },
      queryStringParameters: { b: "bar" },
      body: { c: "baz" },
    };
    // No error
    
    const testTwo: TestRequest = {
      pathParameters: { a: "foo" },
      queryStringParameters: { bee: "bar" }, // Throws an error
      body: { c: "baz" },
    };
  25. Spodera commented on Oct 31, 2020

    @Spodera

    For simple array, I do this (add "readonly type")

    export type ValuesOf<T extends readonly any[]>= T[number];
    
    export const ALL_BASIC_FUNCTIONS_NAMES = ["sub", "add", "div", "mul", "mod"] as const
    
    const keys = ValuesOf<typeof ALL_BASIC_FUNCTIONS_NAMES>
  26. KrzysztofMadejski commented on Dec 31, 2020

    @KrzysztofMadejski

    I use two approaches:

    // Option 0: Define type using object
    const someTypeExample = {
      a: 1,
      b: 'str',
      c: 'foo' as 'foo'|'bar'
    };
    
    type SomeType0 = typeof someTypeExample;
    
    // Option 1: If object stores different types (with disadvantages)
    type SomeType = {
      a: number;
      b: string;
    }
    
    const typeVar: SomeType = {
        a: 10,
        b: 'string'
    }
    
    // used for typechecking
    type SomeTypeKey = keyof SomeType;
    
    // create an array to be used in runtime
    // disadvantage is that properties need to be repeated
    const keys: SomeTypeKey[] = ['a', 'b']; // type checked
    // TODO what I'm missing is:
    // const keys = keys<SomeTypeKey>(); that would return ['a', 'b'] during transpiling
    // ie. native support of https://github.com/kimamula/ts-transformer-keys 
    // which is out of scope for TS: https://github.com/microsoft/TypeScript/issues/13267
    let isValidKey = keys.includes('c' as SomeTypeKey)
    
    
    // Option 2: start from keys array definition, in case all values have the same type
    
    const myKeys = ['foo', 'bar'] as const; // as const does the magic
    type MyKey = typeof myKeys[number]; // = 'foo' | 'bar'
    
    type MyMap = Record<MyKey, string>;
    type MyMap2 = { [key in MyKey]: string };
  27. isumix commented on Jun 26, 2021

    @isumix

    I just managed to make my function return a type checkable object:

    const indexKeys = <K extends string>(keys: readonly K[]) => {
      type Result = Record<K, number>;
    
      const result: Result = {} as Result;
    
      const {length} = keys;
    
      for (let i = 0; i < length; i++) {
        const k = keys[i];
    
        result[k] = i;
      }
    
      return result;
    };

    Here the type checker will complain:

    // Property 'zz' does not exist on type 'Result'.
    const {aa, zz} = indexKeys(['aa', 'bb']); 
  28. paulwongx commented on Dec 26, 2021

    @paulwongx

    As mentioned above by Liam (@7kms) this works to change an array into a type

    const arr= ['foo', 'bar'] as const;
    const myVar:typeof arr[number] = "foo"
    
    // Hovering over myVar shows: 
    // const myVar: "foo" | "bar"
  29. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions