25

I would like to declare a type-enforced array of items and be able to derive a union type from it. This pattern works if you do not explicitly give a type to the items in the array. I am not sure how to best explain it so here is an example:

EXAMPLE 1

type Pair = {
  key: string;
  value: number;
};

const pairs: ReadonlyArray<Pair> = [
  { key: 'foo', value: 1 },
  { key: 'bar', value: 2 },
] as const;

type Keys = typeof pairs[number]['key']

EXAMPLE 2

type Data = {
  name: string;
  age: number;
};

const DataRecord: Record<string, Data> = {
  foo: { name: 'Mark', age: 35 },
  bar: { name: 'Jeff', age: 56 },
} as const;

type Keys = keyof typeof DataRecord;

Here is an example of deriving the keys when using as const. I want this same behavior but with the array being explicitly typed.

const pairs = [
  { key: 'foo', value: 1 },
  { key: 'bar', value: 2 },
] as const;

type Keys = typeof pairs[number]['key']; // "foo" | "bar"

desired value of keys: "foo"|"bar"

actual value of keys: string

11
  • 4
    I don't think you can do this dynamically the way you're trying to, you're conflating runtime values with compile-time types. You will have to give the key attribute of the Pair type the type you want, then it should work as you've written it. Commented Mar 2, 2020 at 20:42
  • 4
    Does this answer your question? Typescript derive union type from tuple/array values Commented Mar 2, 2020 at 20:42
  • @JaredSmith this should not be an issue at runtime. I'm using this to declare an arbitrary number of values that do not change during execution. This would be equivalent to setting key: "foo"|"bar" in the type declaration. Commented Mar 2, 2020 at 20:50
  • "this should not be an issue at runtime" --- typescript does not have runtime, so it is an issue to do it in runtime. Commented Mar 2, 2020 at 21:02
  • 1
    @Ben allow me to be more specific: I don't think you can do this with a tuple of properties pulled out of mutable reference types the way you can with a tuple of immutable primitives. You can say possibleKeys = ['foo', 'bar'] as const; type Keys = typeof possibleKeys[number]; type Pair = { key: Keys, value: number }; but you still need to explicitly enumerate the possible keys. Commented Mar 2, 2020 at 21:15

3 Answers 3

17

For a variable you can either let the compiler infer the type from initialization, or write it out explicitly. If you write it explicitly, as you have, then the initialization value is checked against the annotation, but the actual type of the initializer does not affect the type of the variable (so you lose the type information you want). If you let the compiler infer it, it is no longer possible to constrain the type to conform to a specific interface (as you seem to want)

The solution for this is to use a generic function to both constrain the value and infer it's actual type:

type Pair = {
  key: string;
  value: number;
};
function createPairsArray<T extends readonly Pair[] & Array<{key: V}>, V extends string>(...args: T) {
    return args
}

const pairs = createPairsArray(
  { key: 'foo', value: 1 },
  { key: 'bar', value: 2 },
)

type Keys1 = typeof pairs[number]['key']

type Data = {
  name: string;
  age: number;
};

function createDataObject<T extends Record<string, Data>>(arg: T) {
    return arg;
}
const DataRecord = createDataObject({
  foo: { name: 'Mark', age: 35 },
  bar: { name: 'Jeff', age: 56 },
})

type Keys2 = keyof typeof DataRecord;

Playground Link

Note: For the array case we need to strong arm the compiler a bit into inferring string literal types for key, hence the whole & Array<{key: V}>, where V is a type parameter extending string

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you! This is exactly what I needed!
How would that work in the above example if key was an object instead of "foo" or "bar"?
17

The usual approaches are:

  • let TS infer the type of pairs by omitting the explicit type ReadonlyArray<Pair> (see answer)
  • give key in Pair the type "foo"|"bar"

If you don't want to do this, then the only way to infer your keys and restrict the type of pairs is to use a helper function. The Pair type will also be made generic to save the given key string literal types. You can use an IIFE to make the assignment compact:

type Pair<K = string> = {
    key: K;
    value: number;
};

const pairs = (<T>(p: readonly Pair<T>[]) => p)([
    { key: 'foo', value: 1 },
    { key: 'bar', value: 2 },
] as const) // readonly Pair<"foo" | "bar">[]

type Keys = typeof pairs[number]['key'] // "foo" | "bar"

Playground

Comments

1

@bela53 answer is really good, until it didn't work for me.

The = string part in Pair generic declaration made the compiler infer the type of key as string, which defeated the whole purpose. So here's what fixed it for me:

type ProtoPair<K> = {
    key: K;
    value: number;
}; // no default for type argument - need to provide it explicitly

const pairs = (<T>(p: readonly Pair<T>[]) => p)([
    { key: 'foo', value: 1 },
    { key: 'bar', value: 2 },
] as const)

type Keys = typeof pairs[number]['key'] // "foo" | "bar";
type Pair = ProtoPair<Keys> // Pair with key: "foo" | "bar"

Hope that helps.

1 Comment

I copied your code into a TS Fiddle, and it does not work in v5.6.2... I'm wondering if something has changed with v5 and the code you've posted.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.