64

I have a collection of data that looks like this:

interface Item {
  name: "one" | "two";
  data: string;
}

const namedItems: Item[] = [
  {
    name: "one",
    data: "some data one",
  },
  {
    name: "two",
    data: "some data two",
  },
];

Each item has a name and the value can either be "one" or "two".

Then running an array find on this:

const getData = (query: "one" | "two") =>
  namedItems.find((item): boolean => query === item.name).data;

Throws a typescript error "Object is possibly 'undefined'". Which seems to be because of the fact that finds can possibly not find something but in my example you're only allowed to look for "one" or "two" which would always return a result.

How do I get typescript to know it will always return a result in the find?

5
  • 1
    (This is ugly but it should work) try casting it? Commented Feb 17, 2019 at 22:20
  • 1
    find might not succeed in finding anything in which case .data will not be valid. If you assign the result to a typed variable then it should probably assume it's always valid Commented Feb 17, 2019 at 22:21
  • 2
    Or better yet, do some defensive programming and do handle the case where it's undefined :) Commented Feb 17, 2019 at 22:21
  • 9
    The compiler can't easily verify this for you because find() returns a possibly-null result. But if you're sure that it can't be null you can assert that via the non-null assertion operator... namedItems.find(predicate)!.data Commented Feb 17, 2019 at 22:52
  • Are you sure the collection always include the possible values? Does the container always contain exactly two elements, or can it be less or more? If there are always exactly two elements, do you know they both have unique names? Commented Dec 13, 2021 at 15:53

9 Answers 9

135

Explanation

The reason you are experiencing this is the type signature for Array.prototype.find:

find(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): T | undefined;

As you can see, it always returns T | undefined. This makes a lot of sense because a collection defined as Item[] can contain an arbitrary number of items — including 0. In your case, the collection is complete, but on the type level it's no different than [{ name: "one", data: "some data one" }] or even [].

In order to access item.data in a type-safe manner, TypeScript will require you to double check whether the result is indeed found.

const lookup = namedItems.find(item => item.name === 'one');

if (lookup === undefined) {
  throw new TypeError('The value was promised to always be there!');
}

console.log(lookup.data);

Solution

Since this can become cumbersome in the long run, you may find it useful to create a helper function for this kind of scenarios.

function ensure<T>(argument: T | undefined | null, message: string = 'This value was promised to be there.'): T {
  if (argument === undefined || argument === null) {
    throw new TypeError(message);
  }

  return argument;
}

Usage:

const getData = (query: "one" | "two") =>
  ensure(namedItems.find(item => query === item.name)).data
Sign up to request clarification or add additional context in comments.

5 Comments

I wish I could give this 10 Up votes. Great answer and a very useful aproach to actually use it. Just perfect.
@KarolMajewski Did you use this ensure in production? I wonder how as this doesn't actually ensure any value, you still get a runtime error that you need to handle. Typescript shout here because the code may blow. With 'ensure' it still blows even though TS will be silent.
I do use it in scenarios for which it is made for, namely for asserting the existence of something which I know for a fact exists, but the compiler does not. If an element is allowed to be missing, then such a case must be handled appropriately.
@KarolMajewski ok, so it's just the name that's confusing: ensure function sounds that it ensures that you get something (like if the argument doesn't exist, it'd provide with some fallback value)
With Typescript 3.7, it's possible to employ the assertion function instead of the wrapper.
18

If you're absolutely sure that you'll always get a match then you can tell TS so:

using Item type assertion:

const getData = (query: "one" | "two") => (namedItems.find((item) => query === item.name) as Item).data;

or non-null assertion postfix:

const getData = (query: 'one' | 'two') => namedItems.find((item) => query === item.name)!.data;

Comments

9

Option 1: The simplest hack would be by employing a non-null assertion operator:
(but as @Emanuel Lindström mentioned, it's the laziest, ugliest, and most error-prone hack)

const getData = (query: "one" | "two") =>
  namedItems.find((item): boolean => query === item.name)!.data;
                                                         ^

Option 2: From Typescript 3.7, this can be easily achieved with an assertion function:
(without any hacks, transformations, or workarounds)

const getData = (query: 'one' | 'two'): string => {
  const item = namedItems.find(({ name }) => query === name);
  assert(item); // (A)

  return item.data; // (B)
}

The assertion function assert() in line A influenced the static type of item in line B to be an Item.

In Node.js, the assert() function is available via the built-in module:

import assert from 'assert';

In the browser environment, it's necessary to declare it yourself.
For example, within the utilities/ folder in the project:

function assert(value: unknown): asserts value {}

Comments

5

Use filter instead of find:

V1:

const results: string[] = namedItems.filter((item: Item) => 
    item.name === "one" | item.name ===  "two")
    .map((item:Item) => item.data)

V2:

const results: string[] = namedItems.filter((item: Item) => 
    ["one","two"].indexOf(item.name) !== -1)
    .map((item:Item) => item.data)

Not sure if I understood whether you only wanted a single result... In which case

const results: string[] = namedItems.filter(
    (item: Item, index) => ["one", "two"].indexOf(item.name) !== -1 && index === 0)
    .map((item:Item) => item.data)

Comments

2

How do I get typescript to know it will always return a result in the find?

The easiest, laziest, ugliest and most error-prone solution is to use the non-null assertion operator:

namedItems.find((item): boolean => query === item.name)!.data;

It's a way for you to tell typescript that the value will always be defined, but then again, why are you even using typescript if you're gonna override it? You should guard against possible undefined values "for real" because the code can change in the future and mistakes can happen. So, use any of the previously recommended solutions, or my solution:

const getData = (query: "one" | "two") =>
  (namedItems.find((item) => query === item.name) ?? {name: "undefined", data: "undefined"}).data;

The nullish coalescing operator (??) is a logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined, and otherwise returns its left-hand side operand. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator

If you want a default value you can add whatever you want to the right of the ??-operator. If not, you can do a falsy check: if(!getData())

Comments

1

Array.find() might not succeed and could return undefined.

Since Typescript doesn't know that your namedItems array is not empty at runtime (guaranteed failure in this case), you cannot do anything against it.

However, you can use different methods to extract the data field only when an item is found, for example you can wrap the result in an array, then map if and extract it:

const getData = (query: "one" | "two") =>
  [namedItems.find((item): boolean => query === item.name)]
    .map(x => x && x.data).shift();

const namedItems = [
  {
    name: "one",
    data: "some data one",
  },
  {
    name: "two",
    data: "some data two",
  },
];

const getData = (items, query) =>
  [items.find(item => query === item.name)]
    .map(x => x && x.data).shift();

console.log(getData(namedItems, 'one'));
console.log(getData(namedItems, 'two'));
console.log(getData([], 'one'));

1 Comment

shift() also returns undefined so that wouldn't work.
1

The most concise code is probably this: use The optional chaining operator (?.)and type Item|any

const getData = (query: "one" | "two"):Item|any => namedItems.find((item:Item): boolean => query === item.name)?.data;

1 Comment

This still doesn't guarantee the output of the getData() function will be an Item type. It's can be undefined because of the optional chaining operator.
0

I would also say a good way of dealing with this problem is providing a default (if there is one).

const result = items.find(item.id === myId) || 'A default'

Comments

-1

Set the result of find to a variable of type any|Item which allows it to accept the empty result:

const getData = (query: "one" | "two") => {
  let item:any|Item = namedItems.find((item:Item): boolean => query === item.name);
  return item.data;
};

See CodePen

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.