15

If I have a type which looks like an array:

type names = ['Mike', 'Jeff', 'Ben'];

I can easily define another type which has values of items in names:

type UserName = names[number]

For a function:

function hello(name: UserName) {
  console.log(`Hello, ${name}!`)
}

I can only pass one of Mike, Jeff, Ben to function hello. If I give other values, like John, it can't compile.

What if I don't have a type names, but a const array names?

const names = ['Mike', 'Jeff', 'Ben'];

type UserName = ???;

function hello(name: UserName) {
  console.log(`Hello, ${name}!`)
}

hello('Mike');

Is it possible to define such a type UserName?

Freewind
  • 193,756
  • 157
  • 432
  • 708
  • 1
    Possible duplicate of [Typescript derive union type from tuple/array values](https://stackoverflow.com/q/45251664/2887218) – jcalz Feb 10 '19 at 00:48

2 Answers2

23

In TypeScript 3.4, which should be released in March 2019 it will be possible to tell the compiler to infer the type of a tuple of string literals as a tuple of string literals, instead of as string[], by using the as const syntax. It should look like this:

const names = ['Mike', 'Jeff', 'Ben'] as const; // TS3.4 syntax
type Names = typeof names; // type Names = readonly ['Mike', 'Jeff', 'Ben'] 
type UserName = Names[number]; // 'Mike' | 'Jeff' | 'Ben'

Until then (in TypeScript 3.0 through 3.3) you can get this effect by using a helper function which gives the compiler hints to infer a narrower type:

type Narrowable = string | number | boolean | undefined | null | void | {};
const tuple = <T extends Narrowable[]>(...t: T)=> t;
const names = tuple('Mike', 'Jeff', 'Ben');

type Names = typeof names; // type Names = ['Mike', 'Jeff', 'Ben'] 
type UserName = Names[number]; // 'Mike' | 'Jeff' | 'Ben'

(Note that in both cases you can skip the intermediate Names type and just define type UserName = (typeof names)[number] if you prefer)

Okay, hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
2

Again, one should most of the time:

  1. declare types
  2. set those types to variables

exactly in that order.

Rarely one should do the opposite thing.

If you really need it though, you can do it like this:

const names = ['Mike', 'Jeff', 'Ben'] as ['Mike', 'Jeff', 'Ben'];

type UserName = typeof names;

because you want a tuple type (['Mike', 'Jeff', 'Ben']), but arrays are never inferred as tuples by default, but only as arrays (string[] in this case). However I don't see much sense in doing above thing and again I am suggesting you to do the opposite, idiomatic thing:

type UserName = ['Mike', 'Jeff', 'Ben'];
// however the above type is absolutely static 
// and I don't know if it can provide any benefit so maybe this is more correct:
type UserName = ('Mike' | 'Jeff' | 'Ben')[]

const names: UserName = ['Mike', 'Jeff', 'Ben'] // ok
Nurbol Alpysbayev
  • 19,522
  • 3
  • 54
  • 89
  • 1
    "One should always 1. declare types, 2. set those types to variables. Rarely one should do the opposite thing". Are you really saying that one should never use [type inference](https://www.typescriptlang.org/docs/handbook/type-inference.html)? Type inference is such an integral part of the way TypeScript works that I'm very surprised to see such advice. While it is certainly acceptable for someone to explicitly annotate their types and set values of those types, I wouldn't think of telling anyone to *always* do this. Especially since it often amounts to code duplication. – jcalz Feb 10 '19 at 00:32
  • 1
    @jcalz Well you are right of course, not always. I might convey my thought poorly but I really think that most of the time the algorithm/order is: declaring a type, then using it, not the opposite. Because variables might change and hence the inferred types. This can lead to undesired effects. In other words keeping variables and types separate is a separation of concerns. I changed "always", to "most of the time" which I really meant (because in the end I wrote "rarely one should do the opposite"). – Nurbol Alpysbayev Feb 10 '19 at 05:15