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

keyof for arrays #20965

Closed
bezreyhan opened this issue Jan 2, 2018 · 38 comments
Closed

keyof for arrays #20965

bezreyhan opened this issue Jan 2, 2018 · 38 comments

Comments

@bezreyhan
Copy link

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
@DanielRosenwasser
Copy link
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
Copy link
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
Copy link
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
Copy link
Author

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

@kpdonn
Copy link
Contributor

kpdonn commented Mar 3, 2018

@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
Copy link

tomzaku commented Jun 15, 2018

@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

@nickmccurdy
Copy link
Contributor

nickmccurdy commented Jun 16, 2018

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
Copy link

tpucci commented May 1, 2019

@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
Copy link

tpucci commented May 2, 2019

@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
Copy link

NickDubelman commented May 15, 2019

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
Copy link

benneq commented Jun 17, 2019

Typescript 3.4 made things better:

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

👍

@Serrulien
Copy link

Serrulien commented Jul 10, 2019

@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
Copy link

bopfer commented Jul 12, 2019

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.

@fabb
Copy link

fabb commented Jul 13, 2019

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

@bopfer
Copy link

bopfer commented Jul 13, 2019

@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

@fabb
Copy link

fabb commented Jul 13, 2019

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

@ORESoftware
Copy link

ORESoftware commented Jul 19, 2019

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: #32489

@fabb
Copy link

fabb commented Jul 20, 2019

@ORESoftware look at @benneq‘s answer above.

@ORESoftware
Copy link

ORESoftware commented Jul 20, 2019

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

@fabb
Copy link

fabb commented Jul 20, 2019

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

@ORESoftware
Copy link

ORESoftware commented Jul 20, 2019

@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: #32499

@fabb
Copy link

fabb commented Jul 21, 2019

@ORESoftware you are holding it wrong ;-)

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

@Ash-oi
Copy link

Ash-oi commented Jul 25, 2019

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?

@benneq
Copy link

benneq commented Jul 25, 2019

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

@fabb
Copy link

fabb commented Jul 25, 2019

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

@Ash-oi
Copy link

Ash-oi commented Jul 26, 2019

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

@Ash-oi
Copy link

Ash-oi commented Jul 26, 2019

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

@daniel-nagy
Copy link

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
Copy link

AmirTugi commented Jan 1, 2020

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

@daniel-nagy
Copy link

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
Copy link

fabb commented Jan 1, 2020

@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
Copy link

7kms commented Jul 22, 2020

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
Copy link

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
Copy link

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
Copy link

Spodera commented Oct 31, 2020

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
Copy link

KrzysztofMadejski commented Dec 31, 2020

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
Copy link

isumix commented Jun 26, 2021

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']); 

amaster507 added a commit to amaster507/cookies that referenced this issue Aug 6, 2021
added strict types fixes #291 with help and reference of microsoft/TypeScript#20965 (comment)
@paulwongx
Copy link

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests