Typescript: keyof for arrays

Created on 2 Jan 2018  ·  35Comments  ·  Source: microsoft/TypeScript




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

Most helpful comment

Typescript 3.4 made things better:

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

👍

All 35 comments

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.

Thanks @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".

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>;

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

@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

@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

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.

@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

@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

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?

Typescript 3.4 made things better:

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

👍

@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

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.

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: ''
}
]

@fabb, right, but I wanted to combine that with something like this to have a type with all the valid labels:

type Labels = (typeof pages[number])['label'];

Without the const, the type is just string

I fear you have to choose between your literal defining the type, or explicit typing without const.

I am trying to use keyof like so:

type Events = [
  'repo:push',
  'pullrequest:unapproved',
  'pullrequest:created'
  ]


export interface InterosTag {
  [key: string]: {
    [key: keyof Events]: {   // but this does not work
      "jenkins-job": string,
      "deploy": boolean,
      "eks-key": string
    }
  }
}

any help appreciated - not sure if related or not: https://github.com/microsoft/TypeScript/issues/32489

@ORESoftware look at @benneq‘s answer above.

@fabb i tried earlier today but it didnt work, how would I use it in the context of my code above?

@ORESoftware do it like this:

const events = ['repo:push', 'pullrequest:unapproved', 'pullrequest:created'] as const

export interface InterosTag {
    [key: string]: {
        [key in typeof events[number]]: {
            'jenkins-job': string
            deploy: boolean
            'eks-key': string
        }
    }
}

@fabb ok that worked, I missed the in operator when I tried last time. The remaining problem is it forces me to have _all_ the keys present, not just a subset of the keys.

I fix that, I tried using:

    type Events = typeof events[number];

    [key in Partial<Events>]: {
          'jenkins-job': string
          'deploy': boolean
          'eks-key': string
     }

but Partial is not the right operator. I am looking for a subset of the array values.

I filed a related ticket: https://github.com/microsoft/TypeScript/issues/32499

@ORESoftware you are holding it wrong ;-)

type EventData = Partial<
    {
        [key in Events]: {
            'jenkins-job': string
            deploy: boolean
            'eks-key': string
        }
    }
>

noob here, based on the suggestions here Im trying to write a helper function to check if an object has the required keys

const requiredKeys = <const>[
    'key1',
    'key2',
    'key3'
];


function hasKeys(
  unknownObject: { [key: string]: any },
  requiredKeys: readonly string[]
): unknownObject is { [key in typeof requiredKeys[number]]: unknown } {
  return Object.keys(requiredKeys).every(
    required => unknownObject[required] !== undefined
  );
}

this works but unknownObject ends up being typed as [x: string]: unknown;, any ideas?

@AshleyMcVeigh This should do it for arrays:

function includes<T, U extends T>(arr: readonly U[], elem: T): elem is U {
    return arr.includes(elem as U); // dirty hack, i know
}

Though I'm not sure if you can use const array as object key, because i think it can only be string, number, symbol.

@AshleyMcVeigh you are no noob if you write code like this. It's curious, if you extract { [key in typeof requiredKeys[number]]: unknown } into a named type, it works as expected:

const requiredKeys = <const>[
    'key1',
    'key2',
    'key3'
];

type YAY = { [key in typeof requiredKeys[number]]: unknown }

function hasKeys(
  unknownObject: { [key: string]: any },
  requiredKeys: readonly string[]
): unknownObject is YAY {
  return Object.keys(requiredKeys).every(
    required => unknownObject[required] !== undefined
  );
}

const maybeYay: { [key: string]: any } = { key1: 1, key2: 2, key3: 3, otherKey: "other" }
if (hasKeys(maybeYay, requiredKeys)) {
    console.log(maybeYay.key1) // compiles fine
}

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

anyway thanks for your help @benneq and @fabb

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 }
}

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"]>;

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

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.

@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
}

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;
};

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]

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" },
};

For simple array, I do this (add "readonly type")
````ts
export type ValuesOf

export const ALL_BASIC_FUNCTIONS_NAMES = ["sub", "add", "div", "mul", "mod"] as const

const keys = ValuesOf
````

Was this page helpful?
0 / 5 - 0 ratings