Skip to content

keyof for arrays #20965

Closed
Closed
@bezreyhan

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

DanielRosenwasser

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.

bezreyhan

bezreyhan commented on Jan 2, 2018

@bezreyhan
Author

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

DanielRosenwasser

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

bezreyhan commented on Jan 2, 2018

@bezreyhan
Author

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

kpdonn

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

tomzaku

tomzaku commented on Jun 15, 2018

@tomzaku

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

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.

tpucci

tpucci commented on May 1, 2019

@tpucci

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

tpucci commented on May 2, 2019

@tpucci

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

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?

benneq

benneq commented on Jun 17, 2019

@benneq

Typescript 3.4 made things better:

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

👍

Serrulien

Serrulien commented on Jul 10, 2019

@Serrulien

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

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 remaining items

daniel-nagy

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

AmirTugi commented on Jan 1, 2020

@AmirTugi

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

daniel-nagy

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.

fabb

fabb commented on Jan 1, 2020

@fabb

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

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

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]
derekslarson

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

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

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

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']); 
added a commit that references this issue on Aug 6, 2021
paulwongx

paulwongx commented on Dec 26, 2021

@paulwongx

As mentioned above by @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"
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

        Participants

        @fabb@Sharcoux@bopfer@KrzysztofMadejski@nickserv

        Issue actions

          keyof for arrays · Issue #20965 · microsoft/TypeScript