Rodrigo Mello
Rodrigo Mello

Reputation: 172

Conditional typing in TypeScript

Errors and Touched attributes are used together, and I was wondering if there is any way to tie the use of the two. To use as optional (?) seems to me very shallow.

Would there be a way to make it required to use 'touched' from the moment the 'errors' are filled?!

import { ChangeEvent, FocusEvent, Ref } from 'react';

export interface FormikProps<Value, Element> {
  name: string;
  value: Value;
  onChange: (event: ChangeEvent<Element>) => void;
  onBlur: (event: FocusEvent<Element>) => void;
  error?: string | undefined;
  touched?: boolean | undefined;
  ref?: Ref<Element>;
}

Upvotes: 0

Views: 199

Answers (1)

jcalz
jcalz

Reputation: 327754

Yes, if you can use a type alias instead of an interface, you'd use a union type to represent the constraint (along with an intersection to represent the common properties):

  type FormikProps<Value, Element> = {
    name: string;
    value: Value;
    onChange: (event: ChangeEvent<Element>) => void;
    onBlur: (event: FocusEvent<Element>) => void;
    ref?: Ref<Element>;
  } & (
    | { error: string; touched: boolean }
    | { error?: never; touched?: never });

You can see if you use it that you have to use both or neither:

  const useBoth: FormikProps<string, number> = {
    name: "F",
    value: "v",
    onChange: e => console.log(e),
    onBlur: e => console.log(e),
    error: "hey",
    touched: false
  }; // okay

  const useNeither: FormikProps<string, number> = {
    name: "F",
    value: "v",
    onChange: e => console.log(e),
    onBlur: e => console.log(e)
  }; // okay

or else you get errors:

  const justUseError: FormikProps<string, number> = { // error!
  //    ~~~~~~~~~~~~ <-- "touched is missing"
    name: "F",
    value: "v",
    onChange: e => console.log(e),
    onBlur: e => console.log(e),
    error: "hey",
  };

  const justUseTouched: FormikProps<string, number> = { // error!
  //    ~~~~~~~~~~~~~~ <-- "error is missing"
    name: "F",
    value: "v",
    onChange: e => console.log(e),
    onBlur: e => console.log(e),
    touched: true
  };

You might find it a little annoying to use this type inside a function that checks it, since it seems that there's a bit of an issue with control flow analysis here:

  function f(fp: FormikProps<string, number>) {

    if (typeof fp.error === "string") {
      fp.touched.valueOf(); // error!
    //~~~~~~~~~~ <-- object is possibly undefined ?!
      fp.touched!.valueOf(); // okay, asserted
    } 

    if (fp.error) {
      fp.touched.valueOf(); // this works for some reason
    }

  }

but it's still probably more useful than your original definition.


I agree with a commenter that you might want to encapsulate the two together:

  interface Encapsulated<Value, Element> {
    name: string;
    value: Value;
    onChange: (event: ChangeEvent<Element>) => void;
    onBlur: (event: FocusEvent<Element>) => void;
    ref?: Ref<Element>;
    errTouched?: { error: string; touched: boolean };
  }

That type is straightforward and the type system will understand it more:

  function f2(fp: Encapsulated<string, number>) {
    if (fp.errTouched) {
      fp.errTouched.error.valueOf(); // okay
      fp.errTouched.touched.valueOf(); // okay
    }
  }

Anyway, hope that helps; good luck!

Link to code

Upvotes: 4

Related Questions