Pierre Capo
Pierre Capo

Reputation: 1053

Defining an array of object with accurate types

New to Typescript, I have a quick question about typings an array of object. Right now if I do :

const my_array = [{
    foo: "hello",
    bar: "Typescript"
  },
  {
    foo: "goodbye",
    bar: "JavaScript"
  }
];

It will by default infer my_array with a type of

{foo:string; bar:string;}[]

My request : I would like to have typings more accurate such as:

{foo: "hello" | "goodbye"; bar: // value according to foo }

I also would like to have a DRY solution and define my foo and bar values only once for better maintainability

Upvotes: 0

Views: 66

Answers (3)

Vladislav Ihost
Vladislav Ihost

Reputation: 2187

Current Typescript implementation from version 3.7.5, provides limited support for deducing compile-time constant narrow argument types, but specific construction allows it:

type Narrowable = "" | 0 | true | false | symbol | object | undefined | void | null | {} | [Narrowable] | { [k in keyof any]: Narrowable }

function gen<T extends Narrowable>(x: T): T { return x };

const f = gen(["www", 1,2,3,[1,2,3,[5,"qqq"],"str"],4, {a:5,b:{c:{a:54,b:"www"}}}])

Upvotes: 0

Curtis Fenner
Curtis Fenner

Reputation: 1411

You can define types for each type of record:

type Alpha = {"foo": "alpha", "bar": number}
type Beta = {"foo": "beta", "bar": string}

Then you can define an array which is a list of either Alphas or Betas (called a union type)

let arr: (Alpha | Beta)[] = some_array();

When iterating over the array, TypeScript will know the types of the fields as

for (let el of arr) {
    // el.foo is "alpha" | "beta"
    // el.bar is number | string

But if you check the .foo tag, TypeScript will narrow the type of .bar, because it actually knows each element is either a Alpha or Beta:

    if (el.foo == "alpha") {
        // el.bar is number
    } else {
        // el.bar is string
    }
}

Upvotes: 2

jcalz
jcalz

Reputation: 329943

Coaxing the compiler into inferring literal types for values is tricky when those values are contained as array elements or object properties. Here is one possible way to go about it:

type Narrowable = string | number | boolean | object | {} | null | undefined | void;
const tupleOfNarrowObjects = 
  <V extends Narrowable, O extends { [k: string]: V }, T extends O[]>(...t: T) => t;
const my_array = tupleOfNarrowObjects(
  {
    foo: "hello",
    bar: "Typescript"
  }, {
    foo: "goodbye",
    bar: "JavaScript"
  }
);

If you do that, my_array will now be inferred as the following type:

const my_array: [{
    foo: "hello";
    bar: "Typescript";
}, {
    foo: "goodbye";
    bar: "JavaScript";
}]

which is as narrow as it gets: a tuple of objects whose values are string literals. So the compiler knows that my_array[0].foo is "hello". And if you iterate over my_array the compiler will treat each element as a discriminated union.

How it works:

  • The Narrowable type is essentially the same as unknown, except it is seen by the compiler as a union containing string and number. If you have a generic type parameter V extends N where the constraint N is string or number or a union containing them, then V will be inferred as a literal type if it can be.

  • Usually, when a type parameter O is inferred to be an object type, it does not narrow the property types to literals. However, when O is constrained to an index-signature type whose value type is narrowable to a literal, like { [k: string]: V }, then it will.

  • Finally, when using a rest parameter of generic type constrained to an array type, the compiler will infer a tuple type for that parameter if possible.

Putting all that together, the above tupleOfNarrowObjects infers its argument as a tuple of objects of literal properties if possible. It is ugly (three type parameters for a single argument) but it works: you don't have to repeat yourself.

Hope that helps you. Good luck.

Upvotes: 2

Related Questions