diff --git a/ui/package-lock.json b/ui/package-lock.json
index 2298621..d15438c 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -68,7 +68,8 @@
         "husky": "^6.0.0",
         "jest": "^26.6.3",
         "lint-staged": "^10.5.4",
-        "prettier": "^2.2.1"
+        "prettier": "^2.2.1",
+        "tsc-files": "^1.1.2"
       }
     },
     "node_modules/@apollo/client": {
@@ -13373,6 +13374,18 @@
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
       "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
     },
+    "node_modules/tsc-files": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.2.tgz",
+      "integrity": "sha512-biLtl4npoohZ9MBnTFw4NttqYM60RscjzjWxT538UCS8iXaGRZMi+AXj+vEEpDdcjIS2Kx0Acj++1gor5dbbBw==",
+      "dev": true,
+      "bin": {
+        "tsc-files": "lib/index.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=3"
+      }
+    },
     "node_modules/tslib": {
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
@@ -24801,6 +24814,13 @@
         }
       }
     },
+    "tsc-files": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.2.tgz",
+      "integrity": "sha512-biLtl4npoohZ9MBnTFw4NttqYM60RscjzjWxT538UCS8iXaGRZMi+AXj+vEEpDdcjIS2Kx0Acj++1gor5dbbBw==",
+      "dev": true,
+      "requires": {}
+    },
     "tslib": {
       "version": "1.13.0",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
diff --git a/ui/package.json b/ui/package.json
index 04be30a..f136346 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -57,7 +57,9 @@
     "start": "node --experimental-modules build.mjs watch",
     "build": "NODE_ENV=production node --experimental-modules build.mjs",
     "test": "npm run lint && npm run jest",
-    "lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js",
+    "lint": "npm run lint:types & npm run lint:eslint",
+    "lint:eslint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js",
+    "lint:types": "tsc --noemit",
     "jest": "jest",
     "genSchemaTypes": "npx apollo client:codegen --target=typescript",
     "prepare": "(cd .. && npx husky install)"
@@ -77,7 +79,8 @@
     "husky": "^6.0.0",
     "jest": "^26.6.3",
     "lint-staged": "^10.5.4",
-    "prettier": "^2.2.1"
+    "prettier": "^2.2.1",
+    "tsc-files": "^1.1.2"
   },
   "cache": {
     "swDest": "service-worker.js"
@@ -99,7 +102,8 @@
     }
   },
   "lint-staged": {
-    "*.{js,json,css,md,graphql}": "prettier --write",
-    "*.js": "eslint --cache --fix --max-warnings 0"
+    "*.{ts,tsx,js,json,css,md,graphql}": "prettier --write",
+    "*.{js,ts,tsx}": "eslint --cache --fix --max-warnings 0",
+    "*.{ts,tsx}": "tsc-files --noEmit"
   }
 }
diff --git a/ui/src/components/albumGallery/AlbumBox.js b/ui/src/components/albumGallery/AlbumBox.tsx
similarity index 70%
rename from ui/src/components/albumGallery/AlbumBox.js
rename to ui/src/components/albumGallery/AlbumBox.tsx
index 1743c23..0b6364d 100644
--- a/ui/src/components/albumGallery/AlbumBox.js
+++ b/ui/src/components/albumGallery/AlbumBox.tsx
@@ -1,5 +1,4 @@
 import React, { useState } from 'react'
-import PropTypes from 'prop-types'
 import styled from 'styled-components'
 import { Link } from 'react-router-dom'
 import { ProtectedImage } from '../photoGallery/ProtectedMedia'
@@ -28,7 +27,7 @@ const Image = styled(ProtectedImage)`
   object-position: center;
 `
 
-const Placeholder = styled.div`
+const Placeholder = styled.div<{ overlap?: boolean; loaded?: boolean }>`
   width: 220px;
   height: 220px;
   border-radius: 4%;
@@ -47,14 +46,18 @@ const Placeholder = styled.div`
   `}
 `
 
-const AlbumBoxImage = ({ src, ...props }) => {
+interface AlbumBoxImageProps {
+  src?: string
+}
+
+const AlbumBoxImage = ({ src, ...props }: AlbumBoxImageProps) => {
   const [loaded, setLoaded] = useState(false)
 
   if (src) {
     return (
       <ImageWrapper>
-        <Image {...props} onLoad={loaded => setLoaded(loaded)} src={src} />
-        <Placeholder overlap loaded={loaded ? 1 : 0} />
+        <Image {...props} onLoad={() => setLoaded(true)} src={src} />
+        <Placeholder overlap loaded={loaded} />
       </ImageWrapper>
     )
   }
@@ -62,11 +65,16 @@ const AlbumBoxImage = ({ src, ...props }) => {
   return <Placeholder />
 }
 
-AlbumBoxImage.propTypes = {
-  src: PropTypes.string,
+type AlbumBoxProps = {
+  album?: {
+    id: string
+    title: string
+    thumbnail?: { thumbnail?: { url: string } }
+  }
+  customLink?: string
 }
 
-export const AlbumBox = ({ album, customLink, ...props }) => {
+export const AlbumBox = ({ album, customLink, ...props }: AlbumBoxProps) => {
   if (!album) {
     return (
       <AlbumBoxLink {...props} to="#">
@@ -75,7 +83,7 @@ export const AlbumBox = ({ album, customLink, ...props }) => {
     )
   }
 
-  let thumbnail = album.thumbnail?.thumbnail?.url
+  const thumbnail = album.thumbnail?.thumbnail?.url
 
   return (
     <AlbumBoxLink {...props} to={customLink || `/album/${album.id}`}>
@@ -84,8 +92,3 @@ export const AlbumBox = ({ album, customLink, ...props }) => {
     </AlbumBoxLink>
   )
 }
-
-AlbumBox.propTypes = {
-  album: PropTypes.object,
-  customLink: PropTypes.string,
-}
diff --git a/ui/src/components/messages/SubscriptionsHook.ts b/ui/src/components/messages/SubscriptionsHook.ts
index de1b287..18cefac 100644
--- a/ui/src/components/messages/SubscriptionsHook.ts
+++ b/ui/src/components/messages/SubscriptionsHook.ts
@@ -5,7 +5,7 @@ import { useSubscription, gql } from '@apollo/client'
 import { authToken } from '../../helpers/authentication'
 import { NotificationType } from '../../../__generated__/globalTypes'
 
-const notificationSubscription = gql`
+const NOTIFICATION_SUBSCRIPTION = gql`
   subscription notificationSubscription {
     notification {
       key
@@ -49,7 +49,7 @@ const SubscriptionsHook = ({
   }
 
   const { data, error } = useSubscription<notificationSubscription>(
-    notificationSubscription
+    NOTIFICATION_SUBSCRIPTION
   )
 
   useEffect(() => {
diff --git a/ui/src/components/photoGallery/PhotoGallery.js b/ui/src/components/photoGallery/PhotoGallery.tsx
similarity index 78%
rename from ui/src/components/photoGallery/PhotoGallery.js
rename to ui/src/components/photoGallery/PhotoGallery.tsx
index f3a0c66..41d003a 100644
--- a/ui/src/components/photoGallery/PhotoGallery.js
+++ b/ui/src/components/photoGallery/PhotoGallery.tsx
@@ -3,8 +3,7 @@ import styled from 'styled-components'
 import { Loader } from 'semantic-ui-react'
 import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail'
 import PresentView from './presentView/PresentView'
-import PropTypes from 'prop-types'
-import { SidebarContext } from '../sidebar/Sidebar'
+import { SidebarContext, UpdateSidebarFn } from '../sidebar/Sidebar'
 import MediaSidebar from '../sidebar/MediaSidebar'
 import { useTranslation } from 'react-i18next'
 
@@ -31,6 +30,24 @@ const ClearWrap = styled.div`
   clear: both;
 `
 
+type PhotoGalleryProps = {
+  loading: boolean
+  media: {
+    id: string
+    title: string
+    thumbnail?: {
+      url: string
+    }
+  }[]
+  activeIndex: number
+  presenting: boolean
+  onSelectImage(index: number): void
+  setPresenting(callback: (presenting: boolean) => void): void
+  nextImage(): void
+  previousImage(): void
+  onFavorite(): void
+}
+
 const PhotoGallery = ({
   activeIndex = -1,
   media,
@@ -41,17 +58,15 @@ const PhotoGallery = ({
   nextImage,
   previousImage,
   onFavorite,
-}) => {
+}: PhotoGalleryProps) => {
   const { t } = useTranslation()
   const { updateSidebar } = useContext(SidebarContext)
 
-  const activeImage = media && activeIndex != -1 && media[activeIndex]
+  const activeImage = (media && activeIndex != -1 && media[activeIndex]) || {}
 
-  const getPhotoElements = updateSidebar => {
+  const getPhotoElements = (updateSidebar: UpdateSidebarFn) => {
     let photoElements = []
     if (media) {
-      media.filter(media => media.thumbnail)
-
       photoElements = media.map((photo, index) => {
         const active = activeIndex == index
 
@@ -98,16 +113,4 @@ const PhotoGallery = ({
   )
 }
 
-PhotoGallery.propTypes = {
-  loading: PropTypes.bool,
-  media: PropTypes.array,
-  activeIndex: PropTypes.number,
-  presenting: PropTypes.bool,
-  onSelectImage: PropTypes.func,
-  setPresenting: PropTypes.func,
-  nextImage: PropTypes.func,
-  previousImage: PropTypes.func,
-  onFavorite: PropTypes.func,
-}
-
 export default PhotoGallery
diff --git a/ui/src/components/photoGallery/ProtectedMedia.js b/ui/src/components/photoGallery/ProtectedMedia.tsx
similarity index 54%
rename from ui/src/components/photoGallery/ProtectedMedia.js
rename to ui/src/components/photoGallery/ProtectedMedia.tsx
index a855129..0df0091 100644
--- a/ui/src/components/photoGallery/ProtectedMedia.js
+++ b/ui/src/components/photoGallery/ProtectedMedia.tsx
@@ -1,11 +1,11 @@
-import React from 'react'
-import PropTypes from 'prop-types'
+import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react'
 
 const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype
-const placeholder = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
+const placeholder =
+  'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
 
-const getProtectedUrl = url => {
-  if (url == null) return null
+const getProtectedUrl = (url?: string) => {
+  if (url == undefined) return undefined
 
   const imgUrl = new URL(url, location.origin)
 
@@ -18,25 +18,40 @@ const getProtectedUrl = url => {
   return imgUrl.href
 }
 
+export interface ProtectedImageProps
+  extends DetailedHTMLProps<
+    ImgHTMLAttributes<HTMLImageElement>,
+    HTMLImageElement
+  > {
+  lazyLoading?: boolean
+}
+
 /**
  * An image that needs authorization to load
  * Set lazyLoading to true if you want the image to be loaded once it enters the viewport
  * Native lazy load via HTMLImageElement.loading attribute will be preferred if it is supported by the browser,
  * otherwise IntersectionObserver will be used.
  */
-export const ProtectedImage = ({ src, lazyLoading, ...props }) => {
+export const ProtectedImage = ({
+  src,
+  lazyLoading,
+  ...props
+}: ProtectedImageProps) => {
+  const lazyLoadProps: { 'data-src'?: string; loading?: 'lazy' | 'eager' } = {}
+
   if (!isNativeLazyLoadSupported && lazyLoading) {
-    props['data-src'] = getProtectedUrl(src)
+    lazyLoadProps['data-src'] = getProtectedUrl(src)
   }
 
   if (isNativeLazyLoadSupported && lazyLoading) {
-    props.loading = 'lazy'
+    lazyLoadProps.loading = 'lazy'
   }
 
   return (
     <img
       key={src}
       {...props}
+      {...lazyLoadProps}
       src={
         lazyLoading && !isNativeLazyLoadSupported
           ? placeholder
@@ -47,12 +62,21 @@ export const ProtectedImage = ({ src, lazyLoading, ...props }) => {
   )
 }
 
-ProtectedImage.propTypes = {
-  src: PropTypes.string,
-  lazyLoading: PropTypes.bool,
+export interface ProtectedVideoProps_Media {
+  id: string
+  thumbnail: null | {
+    url: string
+  }
+  videoWeb: {
+    url: string
+  }
 }
 
-export const ProtectedVideo = ({ media, ...props }) => (
+export interface ProtectedVideoProps {
+  media: ProtectedVideoProps_Media
+}
+
+export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => (
   <video
     {...props}
     controls
@@ -63,7 +87,3 @@ export const ProtectedVideo = ({ media, ...props }) => (
     <source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
   </video>
 )
-
-ProtectedVideo.propTypes = {
-  media: PropTypes.object.isRequired,
-}
diff --git a/ui/src/components/routes/AuthorizedRoute.tsx b/ui/src/components/routes/AuthorizedRoute.tsx
index 2ba606e..6779f3d 100644
--- a/ui/src/components/routes/AuthorizedRoute.tsx
+++ b/ui/src/components/routes/AuthorizedRoute.tsx
@@ -1,6 +1,6 @@
 import React, { ReactChild, useEffect } from 'react'
 import PropTypes, { ReactComponentLike } from 'prop-types'
-import { Route, Redirect } from 'react-router-dom'
+import { Route, Redirect, RouteProps } from 'react-router-dom'
 import { useLazyQuery } from '@apollo/client'
 import { authToken } from '../../helpers/authentication'
 import { ADMIN_QUERY } from '../../Layout'
@@ -27,9 +27,9 @@ export const Authorized = ({ children }: { children: JSX.Element }) => {
   return token ? children : null
 }
 
-type AuthorizedRouteProps = {
+interface AuthorizedRouteProps extends Omit<RouteProps, 'component'> {
   component: ReactComponentLike
-  admin: boolean
+  admin?: boolean
 }
 
 const AuthorizedRoute = ({
diff --git a/ui/src/components/routes/Routes.tsx b/ui/src/components/routes/Routes.tsx
index 0739752..6bf93eb 100644
--- a/ui/src/components/routes/Routes.tsx
+++ b/ui/src/components/routes/Routes.tsx
@@ -8,8 +8,8 @@ import { useTranslation } from 'react-i18next'
 
 const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute'))
 
-const AlbumsPage = React.lazy(() =>
-  import('../../Pages/AllAlbumsPage/AlbumsPage')
+const AlbumsPage = React.lazy(
+  () => import('../../Pages/AllAlbumsPage/AlbumsPage')
 )
 const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage'))
 const PhotosPage = React.lazy(() => import('../../Pages/PhotosPage/PhotosPage'))
@@ -18,12 +18,12 @@ const SharePage = React.lazy(() => import('../../Pages/SharePage/SharePage'))
 const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage'))
 
 const LoginPage = React.lazy(() => import('../../Pages/LoginPage/LoginPage'))
-const InitialSetupPage = React.lazy(() =>
-  import('../../Pages/LoginPage/InitialSetupPage')
+const InitialSetupPage = React.lazy(
+  () => import('../../Pages/LoginPage/InitialSetupPage')
 )
 
-const SettingsPage = React.lazy(() =>
-  import('../../Pages/SettingsPage/SettingsPage')
+const SettingsPage = React.lazy(
+  () => import('../../Pages/SettingsPage/SettingsPage')
 )
 
 const Routes = () => {
@@ -32,7 +32,7 @@ const Routes = () => {
   return (
     <React.Suspense
       fallback={
-        <Layout>
+        <Layout title={t('general.loading.page', 'Loading page')}>
           <Loader active>{t('general.loading.page', 'Loading page')}</Loader>
         </Layout>
       }
diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js
index e1d2bdf..0bc82be 100644
--- a/ui/src/components/sidebar/MediaSidebar.js
+++ b/ui/src/components/sidebar/MediaSidebar.js
@@ -370,7 +370,7 @@ const MediaSidebar = ({ media, hidePreview }) => {
     return <SidebarContent media={media} hidePreview={hidePreview} />
   }
 
-  if (error) return error
+  if (error) return <div>{error.message}</div>
 
   if (loading || data == null) {
     return <SidebarContent media={media} hidePreview={hidePreview} />
diff --git a/ui/src/components/sidebar/Sidebar.js b/ui/src/components/sidebar/Sidebar.tsx
similarity index 70%
rename from ui/src/components/sidebar/Sidebar.js
rename to ui/src/components/sidebar/Sidebar.tsx
index 508d263..aed641a 100644
--- a/ui/src/components/sidebar/Sidebar.js
+++ b/ui/src/components/sidebar/Sidebar.tsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
 import styled from 'styled-components'
 import { Icon } from 'semantic-ui-react'
 
-const SidebarContainer = styled.div`
+const SidebarContainer = styled.div<{ highlighted: boolean }>`
   width: 28vw;
   max-width: 500px;
   min-width: 300px;
@@ -38,15 +38,34 @@ const SidebarDismissButton = styled(Icon)`
   }
 `
 
-export const SidebarContext = createContext()
+export type UpdateSidebarFn = (content: React.ReactNode) => void
+
+interface SidebarContextType {
+  updateSidebar: UpdateSidebarFn
+  content: React.ReactNode
+}
+
+export const SidebarContext = createContext<SidebarContextType>({
+  updateSidebar: content => {
+    console.warn(
+      'SidebarContext: updateSidebar was called before initialezed',
+      content
+    )
+  },
+  content: null,
+})
 SidebarContext.displayName = 'SidebarContext'
 
-const Sidebar = ({ children }) => {
-  const [state, setState] = useState({
+type SidebarProps = {
+  children: React.ReactElement
+}
+
+const Sidebar = ({ children }: SidebarProps) => {
+  const [state, setState] = useState<{ content: React.ReactNode | null }>({
     content: null,
   })
 
-  const update = content => {
+  const update = (content: React.ReactNode | null) => {
     setState({ content })
   }
 
diff --git a/ui/src/localization.ts b/ui/src/localization.ts
index 882ad83..f770aab 100644
--- a/ui/src/localization.ts
+++ b/ui/src/localization.ts
@@ -1,5 +1,7 @@
 import i18n from 'i18next'
-import { initReactI18next } from 'react-i18next'
+import { initReactI18next, TFunction } from 'react-i18next'
+
+export type TranslationFn = TFunction<'translation'>
 
 export default function setupLocalization(): void {
   i18n.use(initReactI18next).init({
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
index fd4fb47..76533d9 100644
--- a/ui/tsconfig.json
+++ b/ui/tsconfig.json
@@ -74,5 +74,6 @@
     "skipLibCheck": true /* Skip type checking of declaration files. */,
     "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
   },
-  "exclude": ["node_modules/", "dist/"]
+  "include": ["src/**/*"],
+  "exclude": ["node_modules/**/*", "dist/**/*"]
 }
