Lesson 04: TypeScript - strvcom/frontend-academy-2022 GitHub Wiki

Speaker: Konstantin Lebedev

Resources


Lesson Content

Object types

type Config = {
  api: {
    url: string
    version: number
  }
}

const configObject = {
  api: {
    url: 'http://test.com/api',
    version: 1,
  },
}

Optional modifier

By default, all properties are required, but we can choose to make them optional:

type Product = {
  name?: string
  expirationDate: Date
}

Readonly modifier

Readonly properties can be set when object is created, but afterwards any attempt to change their values is going to result in TS error:

type Product = {
  readonly name: string
  expirationDate: Date
}

// this is fine
const product: Product = { name: 'Milk', expirationDate: new Date() }

// will give a TS error
product.name = 'Shoes'

Intersection types

Allow to create new types by merging the definition of 2 or more existing types:

type InputProps = {
  value: string
  type: string
  className: string
  // ...
}

const Input = (props: InputProps) => {
  return <input {...props} />
}

type EditProps = {
  error?: string
}

// defining an intersection type
type Props = InputProps & EditProps

const InputWithValidation = ({ error, ...rest }: Props) => {
  return (
    <>
      <Input {...rest} />
      {error ? <small>{error}</small> : null}
    </>
  )
}

Union types

Union types define types for variables that can be of 2 or more different types:

type StringLike = string | string[]

// this is fine
const str: StringLike = 'test'
const str: StringLike = ['test', 'test']

// this is not
const str: StringLike = 1000

Type narrowing

When we work with union types, at some point we will want to know the exact type of the variable. We can use JS constructs to hint TS as to what exact type variable holds.

Using typeof operator

type RenderFn = () => string

const renderText = (text: string | RenderFn) => {
  if (typeof text === 'string') {
    return text
  } else {
    return text()
  }
}

Using in operator

type Square = {
  width: number
  height: number
}

type Circle = {
  radius: number
}

type Shape = Square | Circle

const calculateArea = (shape: Shape) => {
  if ('radius' in shape) {
    return Math.PI * shape.radius ** 2
  } else {
    return shape.width * shape.height
  }
}

Using switch statement

type Payload =
  | { kind: 'Error'; code: number }
  | { kind: 'Success'; message: string }
  | { kind: 'NotFound' }
  | { kind: 'NotFound'; code: number }

const getMessage = (response: Payload) => {
  switch (response.kind) {
    case 'Success':
      return `Success! ${response.message}`
    case 'Error':
      return `Error ocurred. Code: ${response.code}`
    case 'NotFound':
      return ''
  }
}

Generics

Generic types allow us to define types with inner types, where the inner type can be dynamic.

type User = {
  name: string
  age: number
}

type Post = {
  title: string
}

// inner type Data is dynamic, it's a parameter for ApiPayload type
type ApiPayload<Data> = {
  statusCode: number
  data: Data
}

// inner type is resolved to Post
const parseApi = (payload: ApiPayload<Post>) => {
  payload.data.title
}

// inner type resolution is deferred
const filterErrors = <T>(payloads: ApiPayload<T>[]) => {
  return payloads.filter((p) => p.statusCode !== 200)
}

// but it gets resolved later
const payloads: ApiPayload<User>[] = []
const responses = filterErrors(payloads)

Utility types

TypeScript comes with a handful of useful utility types, here's a few worth mentioning:

type use
Readonly<T> Applies readonly modifier to all of the type properties
Partial<T> Applies optional (?) modifier to all of the type properties
Required<T> Removes optional (?) modifier from all of the type properties
ReturnType<T> Extracts the return type from a type representing a function signature
Awaited<T> Return the inner type of a Promise type
NonNullable<T> Removes null and undefined from a list of options in a union type
⚠️ **GitHub.com Fallback** ⚠️