3

I need to create an interface that is either a string or an object with one of three keys.

Basically I have a function that depending on the error returns something:

export const determineError = (error: ServerAlerts): AlertError => {
  if (typeof error !== "string") {
    if (error.hasOwnProperty("non_field_errors")) {
      return error.non_field_errors[0];
    } else if (error.hasOwnProperty("detail")) {
      return error.detail;
    } else if (error.hasOwnProperty("email")) {
      return error.email[0];
    } else {
      return UNKNOWN_ERROR;
    }
  } else {
    return error;
  }
};

Here are the types:

export type AlertError =
  | "Unable to log in with provided credentials."
  | "E-mail is not verified."
  | "Password reset e-mail has been sent."
  | "Verification e-mail sent."
  | "A user is already registered with this e-mail address."
  | "Facebook Log In is cancelled."
  | string;

export interface ServerAlerts {
  non_field_errors: [string];
  detail: string;
  email: [string];
}

But the way I designed ServerAlerts here does not work for me, since ServerAlerts can also be a string and if it has one of its keys, it only has one.

How would you design such a type or interface?

EDIT: I tried making the keys optional by giving them a question mark, but then my linter complains in the respective key's error return statement in determineError.

danronmoon
  • 3,814
  • 5
  • 34
  • 56
J. Hesters
  • 13,117
  • 31
  • 133
  • 249
  • Re `non_field_errors`, `detail`, and `email` -- are they mutually-exclusive? Is any (or all) optional? – T.J. Crowder Oct 06 '18 at 17:27
  • They are all optional. There can be one, two of them, or all three of them at the same time. If there is none, it is just a string that is coming through. – J. Hesters Oct 06 '18 at 19:21
  • Aside from the `| string` union suggested below, this is a duplicate of [How to create a Partial-like that requires a single property to be set](//https://stackoverflow.com/questions/48230773/how-to-create-a-partial-like-that-requires-a-single-property-to-be-set/48244432). – jcalz Oct 06 '18 at 23:56
  • Your linter is complaining that... the property might be `undefined`, I guess? Yes, the question mark makes it possible (in `--strictNullChecks` mode) to pass in `undefined` for the property, so you need to check for that too (or use an assertion). – jcalz Oct 07 '18 at 00:00
  • 1
    Do you really intend for `non_field_errors` and `email` to be 1-element arrays (a.k.a. a 1-tuple or singleton)? That's what `[string]` means: a single string in an array. If the arrays can have more than one element, you should use the syntax `string[]` or `Array` instead. – jcalz Oct 07 '18 at 00:01

1 Answers1

2

If I'm understanding you correctly, just declare the parameter as being either ServerAlerts or string:

export const determineError = (error: ServerAlerts|string): AlertError => {
// -----------------------------------------------^^^^^^^

In a comment you've said that all three of the ServerAlerts properties are optional, so you need to mark them as such with ?:

interface ServerAlerts {
  non_field_errors?: [string];
  detail?: string;
  email?: [string];
}

However, that means that anything typed object will also work, because all the fields are optional. So if you do both of those things, you get:

determineError("foo");                       // Works
determineError({ non_field_errors: ["x"] }); // Works
determineError({ detail: "x" });             // Works
determineError({ email: ["x"] });            // Works
determineError({});                          // Works (because all fields are optional)
let nonLiteralServerAlerts: object;
nonLiteralServerAlerts = { foo: ["x"] };
determineError(nonLiteralServerAlerts);      // Works (because all fields are optional)
determineError({ foo: ["x"] });              // Fails (correctly)

Playground example

Which suggests you might just use object in the parameter signature. If you want to require one of the three fields (which I believe would do away with that UNKNOWN_ERROR branch), you'd define three interfaces and make ServerAlerts a union of them:

interface ServerAlertsNonFieldErrors {
  non_field_errors: [string];
}

interface ServerAlertsDetail {
  detail: string;
}

interface ServerAlertsEmail {
  email: [string];
}

type ServerAlerts = ServerAlertsNonFieldErrors | ServerAlertsDetail | ServerAlertsEmail;

Then you'd use type assertions when returning the specific field:

if (error.hasOwnProperty("non_field_errors")) {
  return (error as ServerAlertsNonFieldErrors).non_field_errors[0];
// ------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If you do that, then you get:

determineError("foo");                       // Works
determineError({ non_field_errors: ["x"] }); // Works
determineError({ detail: "x" });             // Works
determineError({ email: ["x"] });            // Works
determineError({});                          // Fails (correctly)
let nonLiteralServerAlerts: object;
nonLiteralServerAlerts = { foo: ["x"] };
determineError(nonLiteralServerAlerts);      // Fails (correctly)
determineError({ foo: ["x"] });              // Fails (correctly)

Playground Example

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thank you! That solves half of the problem. It addresses the possibility for a string. But what it's missing is for the interface's keys (non_field_errors, detail, email) to be checked. – J. Hesters Oct 06 '18 at 19:22
  • @J.Hesters - As you said in your question, if they're optional (and you've confirmed now they are), use `?` to mark them so. I've updated the answer. – T.J. Crowder Oct 07 '18 at 07:52
  • Any object now matches `ServerAlerts` since all fields are optional. – Motti Oct 07 '18 at 08:14
  • 1
    That's because you're using a literal object, when using literal objects typescript complains about extra fields. If you use a non-literal object it will not fail (playground link is too long for comment so just add these three lines to your example) `let nonLiteralServerAlerts; nonLiteralServerAlerts = { foo: ["x"] }; determineError(nonLiteralServerAlerts); ` – Motti Oct 07 '18 at 08:47
  • 1
    @Motti - Thanks! I've folded that into the answer. – T.J. Crowder Oct 07 '18 at 08:56