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

Creating types from values in array #28046

Closed
G2Jose opened this issue Oct 22, 2018 · 28 comments
Closed

Creating types from values in array #28046

G2Jose opened this issue Oct 22, 2018 · 28 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@G2Jose
Copy link

G2Jose commented Oct 22, 2018

TypeScript Version: 3.0.3

Search Terms: Type based on values in array

Is there a current or planned feature to create a type from strings in an array?

Code

const values = ['A', 'B']
type Foo = OneOf<values> // Is there a way of doing this?

const v1: Foo = 'A' // This should work
const v2: Foo = 'D' // This should give me an error since 'D' doesn't exist in values

Similar to how keyof works:

const values = {
  A: 'A',
  B: 'B'
}
type Foo = keyof typeof values
const v1: Foo = 'A'
const v2: Foo = 'D' // Type '"D"' is not assignable to type '"A" | "B"'

Related Issues: #20965

Link to playground http://www.typescriptlang.org/play/#src=let%20vals1%20%3D%20%5B'A'%2C%20'B'%5D%0D%0Atype%20Foo1%20%3D%20OneOf%3Cvals1%3E%20%2F%2F%20Is%20there%20a%20way%20of%20doing%20this%3F%0D%0A%0D%0Alet%20v1%3A%20Foo1%20%3D%20'A'%20%2F%2F%20This%20should%20work%0D%0Alet%20v2%3A%20Foo1%20%3D%20'D'%20%2F%2F%20This%20should%20give%20me%20an%20error%20since%20'D'%20doesn't%20exist%20in%20values%0D%0A%0D%0Alet%20vals2%20%3D%20%7B%0D%0A%20%20A%3A%20'A'%2C%0D%0A%20%20B%3A%20'B'%0D%0A%7D%0D%0Atype%20Foo2%20%3D%20keyof%20typeof%20vals2%0D%0Alet%20v3%3A%20Foo2%20%3D%20'A'%0D%0Alet%20v4%3A%20Foo2%20%3D%20'D'%20%2F%2F%20Type%20'%22D%22'%20is%20not%20assignable%20to%20type%20'%22A%22%20%7C%20%22B%22'

@fatcerberus
Copy link

keyof only works because it uses information known statically at compile time. Types in TS are fully erasable and don't exist in the transpiled code, so it's simply not possible to create a type based on the runtime contents of an array.

@ghost
Copy link

ghost commented Oct 22, 2018

This is possible:

function stringLiterals<T extends string>(...args: T[]): T[] { return args; }
type ElementType<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer ElementType> ? ElementType : never;

const values = stringLiterals('A', 'B');
type Foo = ElementType<typeof values>;

const v1: Foo = 'A' // This should work
const v2: Foo = 'D' // This should give me an error since 'D' doesn't exist in values

Definitely not clear how to do this though. #27179 is related.

@fatcerberus
Copy link

That’s still using information known statically by the compiler though; at that point I don’t understand what the advantage is over just saying ”A” | “B”.

@ghost
Copy link

ghost commented Oct 22, 2018

@fatcerberus It's useful to avoid repeating information -- if you just write const values = ["A", "B"]; type Foo = "A" | "B"; it's easy for someone to change one of them while forgetting to change the other. You could write const values: Foo[] =["A", "B"];, but that's still susceptible to adding an additional entry to Foo and forgetting to put it in values.

@ahejlsberg ahejlsberg added the Question An issue which isn't directly actionable in code label Oct 22, 2018
@ChibiBlasphem
Copy link

ChibiBlasphem commented Mar 10, 2019

Where are we on this, now ?

Like @andy-ms said it would be really useful to avoid repeating information manually.
And like @fatcerberus said, types are fully erasable. We sometimes need to runtime check data we get for example from a non pure part of application (like webservice, or from the user).

@drop-george
Copy link

Building on @andy-ms's answer, and using const assertions introduced in typescript 3.4, it's now possible to do something like this

const values = ['A', 'B'] as const
type ElementType < T extends ReadonlyArray < unknown > > = T extends ReadonlyArray<
  infer ElementType
>
  ? ElementType
  : never

type Foo = ElementType<typeof values> // this is correctly inferred as literal "A" | "B"

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

@7kms
Copy link

7kms commented Apr 1, 2020

in [email protected] and above. solve it like this

export const type = <const>[
  'room',
  'room_with_gifter',
  'user_send'
];

export interface Activity {
  id?: string;
  type: typeof type[number];
}

@AmirSavand
Copy link

AmirSavand commented May 10, 2020

Instead of this:

export const items = <const>[
  'room',
  'room_with_gifter',
  'user_send'
];

export interface Activity {
  id?: string;
  type: typeof items[number];
}

You can do this:

export const items = [
  'room',
  'room_with_gifter',
  'user_send'
] as const;

export type Item = typeof items;

export interface Activity {
  id?: string;
  type: Item;
}

image

Really don't like using type as a variable name.

@vegerot
Copy link

vegerot commented Jun 8, 2020

@7kms

in [email protected] and above. solve it like this

export const type = <const>[
  'room',
  'room_with_gifter',
  'user_send'
];

export interface Activity {
  id?: string;
  type: typeof type[number];
}

Now what if I have this in a class?

class Class {
  const list = <const>[
   'room',
   'room_with_gifter',
   'user_send'
 ];

 const withType: typeof list[number];
}

Does not work. I also cannot do this.list[number]. Also, for my particular
usecase, I cannot set list to be static

@eyalch
Copy link

eyalch commented Jun 9, 2020

@7kms

in [email protected] and above. solve it like this

export const type = <const>[
  'room',
  'room_with_gifter',
  'user_send'
];

export interface Activity {
  id?: string;
  type: typeof type[number];
}

Now what if I have this in a class?

class Class {
  const list = <const>[
   'room',
   'room_with_gifter',
   'user_send'
 ];

 const withType: typeof list[number];
}

Does not work. I also cannot do this.list[number]. Also, for my particular
usecase, I cannot set list to be static

@vegerot You can't use const directly in the scope of the class, so the syntax is incorrect.

@vegerot
Copy link

vegerot commented Jun 9, 2020

@7kms

in [email protected] and above. solve it like this

export const type = <const>[
  'room',
  'room_with_gifter',
  'user_send'
];

export interface Activity {
  id?: string;
  type: typeof type[number];
}

Now what if I have this in a class?

class Class {
  const list = <const>[
   'room',
   'room_with_gifter',
   'user_send'
 ];

 const withType: typeof list[number];
}

Does not work. I also cannot do this.list[number]. Also, for my particular
usecase, I cannot set list to be static

@vegerot You can't use const directly in the scope of the class, so the syntax is incorrect.

@7kms My bad. I copied and pasted too much. What I meant to say is

class Class {
  private list = <const>[
   'room',
   'room_with_gifter',
   'user_send'
 ];

 private withType: typeof list[number];

// OR
  private withType2: typeof this.list[number];

}

etc. all do not work.
Also, for my particular usecase, I cannot set list to be static

@eyalch
Copy link

eyalch commented Jun 9, 2020

@7kms

in [email protected] and above. solve it like this

export const type = <const>[
  'room',
  'room_with_gifter',
  'user_send'
];

export interface Activity {
  id?: string;
  type: typeof type[number];
}

Now what if I have this in a class?

class Class {
  const list = <const>[
   'room',
   'room_with_gifter',
   'user_send'
 ];

 const withType: typeof list[number];
}

Does not work. I also cannot do this.list[number]. Also, for my particular
usecase, I cannot set list to be static

@vegerot You can't use const directly in the scope of the class, so the syntax is incorrect.

@7kms My bad. I copied and pasted too much. What I meant to say is

class Class {
  private list = <const>[
   'room',
   'room_with_gifter',
   'user_send'
 ];

 private withType: typeof list[number];

// OR
  private withType2: typeof this.list[number];

}

etc. all do not work.
Also, for my particular usecase, I cannot set list to be static

class Class {
  private list = [
    "room",
    "room_with_gifter",
    "user_send"
  ] as const

  private withType: Class["list"][number]
}

@vegerot How about this?

@vegerot
Copy link

vegerot commented Jun 9, 2020

@eyalch that's awesome! Thank you. I thought the solution would involve typeof or something. Would you give a quick explanation for how this works, or link me to the handbook where this is discussed?

@eyalch
Copy link

eyalch commented Jun 9, 2020

@eyalch that's awesome! Thank you. I thought the solution would involve typeof or something. Would you give a quick explanation for how this works, or link me to the handbook where this is discussed?

@vegerot Sure, I believe it's an "Index type". Here it is in the Handbook: https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types

Glad I could help!

@a-x-
Copy link

a-x- commented Jul 31, 2020

Hey %username%, if ElementType solution doesn't work for you too,
take attention on as const after array. It's crucial

@soryy708
Copy link

soryy708 commented Nov 7, 2021

const animals = ['cat', 'dog', 'mouse'] as const
type Animal = typeof animals[number]

// type Animal = 'cat' | 'dog' | 'mouse'

https://steveholgado.com/typescript-types-from-arrays/

@zeckdude
Copy link

zeckdude commented Dec 5, 2021

Not sure why this works (maybe someone can help me understand), but this seems to work great for me:

const expectedValues = {
  headlines: {
    signUpSelection: 'Create an account',
    emailSignUp: 'Sign up (for free)',
    logInSelection: 'Log into your account',
    emailLogIn: 'Welcome back!',
    checkout: 'Add a payment method',
  },
};

type SectionName = keyof typeof expectedValues.headlines;

@vegerot
Copy link

vegerot commented Dec 7, 2021

Not sure why this works (maybe someone can help me understand), but this seems to work great for me:

const expectedValues = {
  headlines: {
    signUpSelection: 'Create an account',
    emailSignUp: 'Sign up (for free)',
    logInSelection: 'Log into your account',
    emailLogIn: 'Welcome back!',
    checkout: 'Add a payment method',
  },
};

type SectionName = keyof typeof expectedValues.headlines;

@zeckdude I think you mean

const expectedValues = {
  headlines: {
    signUpSelection: 'Create an account',
    emailSignUp: 'Sign up (for free)',
    logInSelection: 'Log into your account',
    emailLogIn: 'Welcome back!',
    checkout: 'Add a payment method',
  },
} as const;

type SectionName = keyof typeof expectedValues.headlines;

@matthew-dean
Copy link

What if we want to use these as object keys?

export const weekdays = [
  'sunday',
  'monday',
  'tuesday',
  'wednesday',
  'thursday',
  'friday',
  'saturday'
] as const

export type WeekdayName = typeof weekdays

// This causes the error: 
// Type 'readonly ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]' does not satisfy the constraint 'string | number | symbol'
const WeekDayObj: Record<WeekdayName, any> = {
  
}

These are string types but it seems that TS doesn't recognize this for the purposes of creating object keys.

@soryy708
Copy link

@matthew-dean
type WeekdayName = typeof weekdays[number];

@jniac
Copy link

jniac commented Sep 6, 2022

Desired:

What about the ability of inferring from an argument?
It could be useful to force the user of the following function to use one of previous declared options:

const myFunc = <T extends readonly unknown[]>(options: T, currentOption: T[number]) => {
  // ...
}
myFunc(['foo', 'bar', 'baz'], 'bar') // ok
myFunc(['foo', 'bar', 'baz'], 'qux') // error

Currently it's not possible since as const could not be used in function parameters.

Workaround:

So the following usage works:

const options = ['foo', 'bar', 'baz'] as const
myFunc(options, 'bar') // ok
myFunc(options, 'qux') // error

@xxRockOnxx
Copy link

xxRockOnxx commented Sep 27, 2022

Similar to @jniac example, my use case is more like

interface TabsProp<T extends string[]> {
  tabs: T
  selected: OneOf<T>
  onSelect: (tab: OneOf<T>) => void
}

I currently have no workaround

@karol-majewski
Copy link

karol-majewski commented Oct 13, 2022

@xxRockOnxx Try this:

import * as React from 'react';

interface TabsProp<T extends string> {
    tabs: T[]
    selected: ExcludeFromTypeInference<T>;
    onSelect: (tab: ExcludeFromTypeInference<T>) => void
}

/**
 * @see https://github.com/microsoft/TypeScript/issues/14829#issuecomment-504042546
 */
type ExcludeFromTypeInference<T> = [T][T extends any ? 0 : never];

declare class MyComponent<T extends string> extends React.Component<TabsProp<T>> {};

<MyComponent tabs={['foo', 'bar']} selected={'foo'} onSelect={tab => {
    tab === 'foo' || tab === 'bar';
}} />

TypeScript Playground

@SamB
Copy link

SamB commented Mar 8, 2023

Huh, I was expecting this to be a request for a feature where, starting with an expression like

["foo", "bar"]

you could request a "refactor" or something that would generate something equivalent to:

type NewType =
|"foo"
|"bar"

(where NewType would be selected so you could type the name you actually want),
because that is something that I've wanted to do before.

@shiftyp
Copy link

shiftyp commented May 11, 2023

EDIT:

I see typeof weekdayNames[number] works as well. Oops

ORIGINAL:

Not sure if this helps, but to convert a tuple to a union type its sufficient to do:

const weekdayNames = [
    "monday",
    "tuesday"
    /* ect */
] as const

// Gives "monday" | "tuesday"
type WeekdayNames = typeof weekdayNames[Exclude<keyof typeof weekdayNames, keyof Array>]

@VictorLandimBB
Copy link

This is how to do it in late 2023:

const animals = ['cat', 'dog', 'mouse'] as const
type Animal = typeof animals[number]

// type Animal = 'cat' | 'dog' | 'mouse'

from: https://steveholgado.com/typescript-types-from-arrays/

@seanpmaxwell
Copy link

Desired:

What about the ability of inferring from an argument? It could be useful to force the user of the following function to use one of previous declared options:

const myFunc = <T extends readonly unknown[]>(options: T, currentOption: T[number]) => {
// ...
}
myFunc(['foo', 'bar', 'baz'], 'bar') // ok
myFunc(['foo', 'bar', 'baz'], 'qux') // error
Currently it's not possible since as const could not be used in function parameters.

Workaround:

So the following usage works:

const options = ['foo', 'bar', 'baz'] as const
myFunc(options, 'bar') // ok
myFunc(options, 'qux') // error

I got it to work by putting as const in the function call. i.e. myFun(['asdf', ffff'] as const);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests