0

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

2
  • When you say "value according to foo" I assume if foo is "hello" then bar is of a type, but if foo is "goodbye", then bar is of another type. Is that correct? Commented Dec 24, 2018 at 17:31
  • Yes exactly. In this case if foo is type "hello", then bar should be a type of "Typescript" Commented Dec 24, 2018 at 17:32

3 Answers 3

2

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.

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

1 Comment

Awesome, that is exactly what I was looking for. Thanks a lot for the answer and the good explanation.
2

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

Comments

0

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"}}}])

Comments

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.