Advanced types

Sara Lissette

Desarrolladora Fullstack en

@LissetteIbnz

interface FormState {
  name: string;
  age: number;
  country: string;
}

export const SampleComponent: React.FC = () => {
  const [formState, setFormState] = React.useState<FormState>(null);

  // fieldName: "name" | "age" | "country"
  const handleOnChange = (fieldName: keyof FormState) => (
    event: React.ChangeEvent<HTMLInputElement>,
  ) => setFormState(oldValues => ({ ...oldValues, [fieldName]: event.target.value }));

  return (
    <>
      <input onChange={handleOnChange("name")} value={formState.name}/>
      <input onChange={handleOnChange("age")} value={formState.age}/>
      <input onChange={handleOnChange("country")} value={formState.country}/>
    </>
  );
};

{ keyof T }

Crea un tipo string literal con las claves de T

interface FormState {
  id: number;
  name: string;
  age: number;
  country: string;
}

type Validation = Pick<FormState, "name" | "age">;

···

const handleOnChange = (fieldName: keyof FormState) => (
    event: React.ChangeEvent<HTMLInputElement>,
  ) => {
    ···  
    if (fieldName !== "id" && fieldName !== "country") { // Error si omitimos esta guarda
      fieldValidation(fieldName, formState);
    }
  };

···

function fieldValidation<T extends FormState>(fieldName: keyof Validation, values: T): void;

{ Pick <T, K> }

Construye un tipo con todas las propiedades de T que son claves de K y existen en T

type Pick<T, K extends keyof T> = { [P in K]: T[P] };

{ Omit <T, K**> }

Construye un tipo con todas las propiedades de T excepto las que son claves de K

type Omit<T, K extends keyof any> = 
	Pick<T, Exclude<keyof T, K>>;
import { SelectProps as SelectPropsMUI } from "@material-ui/core/Select";

interface SelectProps extends Omit<SelectPropsMUI, "multiple" | "multiline"> {
  label?: string;
  options: string[];
  readOnly?: boolean;
}

const SelectComponent: React.FC<SelectProps> = ({
  classes,
  disabled, 
  label,
  onChange,
  options,
  readOnly,
  required,
  value = "",
  ...other
}) => {
  
  ···

Omit paso a paso:

T = "a" | "b" | "c"

K = "b"

 

Exclude<T, K> = <"a" | "b" | "c", "b"> = "a" | "c"

Pick<T, result Exclude<T, K>> = <"a" | "b" | "c", "a" | "c"> = "a" | "c"

{ OmitOwn <T, K> }

Tipo personalizado que construye un tipo con todas las propiedades de T que son claves de K y existen en T

type Omit<T, K extends keyof T> = 
	Pick<T, Exclude<keyof T, K>>;
interface FormState {
  name: string;
  age: number;
  country: string;
}

type Validation = OmitOwn<FormState, "age" | "country">;

{ Partial <T> }

Construye un tipo con todas las propiedades de T establecidas como opcionales

const mapUserAMToVM = (user: User): UserVM =>
  user && { age: user.age, email: user.email, lastName: user.lastName, name: user.name };

describe("mapUserAMToVM", () => {
  const testUser = ({
    age = 20,
    email = "Irrelevant email",
    id = 1,
    lastName = "Irrelevant lastName",
    name = "Irrelevant name",
  }: Partial<User>): User => ({ age, email, lastName, name, id });

  it("should be a valid UserVM when pass a valid user", () => {
    const newUser: User = testUser({ id: 5, email: "test@email.com" });
    const result = mapUserAMToVM(newUser);
    expect(result).toEqual({
      age: 20,
      email: "test@email.com",
      lastName: "Irrelevant lastName",
      name: "Irrelevant name",
    });
  });
});
type Partial<T> = { [P in keyof T]?: T[P] };

{ Required <T> }

Construye un tipo con todas las propiedades de T establecidas como requeridas

interface UserVM {
  name: string;
  email: string;
  age?: number; // optional
  country?: string; // opcional
}

type UserAPI = Required<UserVM>;

const setUser = (user: UserAPI): void => {
  fetch("https://api.gofio.com/user", {
    method: "post",
    body: JSON.stringify(user),
  })
    .then(response => response.json())
    .then(data => notify(data));
};

const user: UserVM = {
  email: "email@email.com",
  name: "Sample",
};

setUser(user); /** ERROR Argument of type 'UserVM' is not assignable to parameter of type 'Required<UserVM>'.
  Property 'age' is optional in type 'UserVM' but required in type 'Required<UserVM>' */
type Required<T> = { [P in keyof T]-?: T[P] };

{ Readonly <T> }

Construye un tipo con el conjunto de propiedades de T establecidas como readonly

interface FormState {
  readonly id: number; // <- sólo lectura
  name: string;
  age: number;
  country: string;
}

type FormStartReadOnly = Readonly<FormState>;
type FormStartReadOnly = {
    readonly id: number;
    readonly name: string;
    readonly age: number;
    readonly country: string;
}
type Readonly<T> = { readonly [P in keyof T]: T[P] };

{ Record <K, T> }

Construye un tipo con un conjunto de propiedades especificadas en K del tipo T

interface Data {
  root: boolean;
  path: string;
  auth: boolean;
  timeExpire: number;
}

type SampleUsers = "admin" | "user" | "guest";

const sampleUsers: Record<SampleUsers, Data> = {
  admin: { root: true, auth: true, path: "/users/admin", timeExpire: 2 },
  user: { root: false, auth: true, path: "/users/user", timeExpire: 5 },
  guest: { root: false, auth: false, path: "/users/public", timeExpire: 0 },
};
type Record<K extends keyof any, T> = { [P in K]: T };

{ ReturnType <function> }

Construye un tipo con el retorno de la función

const ClientFactory = (client: FormState) => ({
  id: Math.random(),
  date: new Date(),
  active: true,
  ...client,
});

type ClientData = ReturnType<typeof ClientFactory>;
type ReturnType<T extends (...args: any) => any> = 
	T extends (...args: any) => infer R ? R : any;

{ Tipos mapeados y genéricos }

Ejemplo de cómo construir un nuevo tipo a partir uno existente y cambiar sus propiedades

interface FormState {
  name: string;
  email: string;
}

interface ValidationResult<T> {
  hasError: boolean;
  message: "Field is required" | "";
  value: T;
}

type ValidationField<T> = { [P in keyof T]: ValidationResult<T[P]> };

const requiredFieldValidation = (state: FormState): ValidationField<FormState> =>
  Object.keys(state)
    .map(key => ({
      [key]: {
        hasError: !state[key],
        message: state[key] ? "" : "Field is required",
        value: state[key],
      } as ValidationResult<FormState>,
    }))
    .reduce((accumulator, current) => ({ ...accumulator, ...current }), {} as ValidationField<FormState>);
type ValidationField<T> = 
	{ [P in keyof T]: ValidationResult<T[P]> };
    + Object {
    +   "email": Object {
    +     "hasError": true,
    +     "message": "Field is required",
    +     "value": "",
    +   },
    +   "name": Object {
    +     "hasError": false,
    +     "message": "",
    +     "value": "Irrelevant name",
    +   },
    + }

Preguntas, dudas, cervezas...?

GRACIAS

Tipos avanzados con TypeScript

By Sara Lissette

Tipos avanzados con TypeScript

Presentación para el #SummerTech2019 de GDG Tenerife en colaboración con AdaLoveDev - agosto2019

  • 1,122