TypeScript

Workshop

+ Vue.js 3

1

Intro to TypeScript with Vue.js

Why Use TypeScript with Vue.js?

Vue.js 3 is written in TypeScript

1

So are these popular Vue Tools

1

1

🤔 Must be something to TypeScript

Prevent errors as you develop

2

Prevent errors as you develop

2

Debugging in plain JS takes running code

Prevent errors as you develop

2

TypeScript surfaces many more errors in the IDE

<script setup lang="ts">
import { ref } from "vue";
const notes = ref([{ title: "", body: "", createdAt: new Date() }]);
const createNote = () => {
  notes.push({
    title: "My new note",
    body: "hello world",
    createdAt: "Thu Nov 30 2023 16:21:12 GMT-0600 (Central Standard Time)",
  });
};
</script>

2

Prevent errors as you develop

Can you spot the error?

2

Prevent errors as you develop

No .value on the notes ref

<script setup lang="ts">
import { ref } from "vue";
const notes = ref([{ title: "", body: "", createdAt: new Date() }]);
const createNote = () => {
  notes.push({
    title: "My new note",
    body: "hello world",
    createdAt: "Thu Nov 30 2023 16:21:12 GMT-0600 (Central Standard Time)",
  });
};
</script>

2

Prevent errors as you develop

createdAt is not a Date object

<script setup lang="ts">
import { ref } from "vue";
const notes = ref([{ title: "", body: "", createdAt: new Date() }]);
const createNote = () => {
  notes.value.push({
    title: "My new note",
    body: "hello world",
    createdAt: "Thu Nov 30 2023 16:21:12 GMT-0600 (Central Standard Time)",
  });
};
</script>

Makes Refactors Less Risky and Less Stressful

3

3

Websites and apps are ever evolving projects.

  1. Business requirements will change
  2. Scope will grow
  3. Refactoring is unavoidable 99% of the time

Let's See an Example in My IDE

3

  • red squiggly lines in IDE
  • npx nuxi typecheck
  • npx vue-tsc --noEmit

examples/1-RefactorExample.vue

3

BTW: There are many tools for generating types from Database schema

Gives Autocomplete Superpowers

4

Let's See an Example in My IDE

4

examples/2-AutocompleteExample.vue

Setup a Vue Project for TypeScript

Bootstrapping a TypeScript + Vue project is easy!

Included as an option when bootstrapping a vanilla Vue.js project

Included with Nuxt by default

npx nuxi init

This is how I created our project for the coding exercises

Add to an existing project

  • Vue CLI - @vue/cli-plugin-typescript
  • Vite - TS is built in, just change out some filenames to .ts, and add some ts config files

Now you can use TypeScript!

  • In .ts files
  • in .vue files
<!--App.vue-->

<!-- with the Composition API (👉 recommended)-->
<script setup lang="ts"></script>

<!-- with the composition API -->
<script lang="ts"></script>

IDE Setup

For VS Code

(Webstorm provides support out of the box)

IDE Setup

Step #1: Install Vue Language Features (previously Volar)

IDE Setup

Step #2: No step 2, that's it

🎉

IDE Setup is very important!

TypeScript doesn't run in the browser.

This the IDE TypeScript's only chance to be useful. Make sure your setup works!

Questions?

🙋🏾‍♀️🙋

Exercise #1

👩‍💻👨🏽‍💻

2

How to Type Reactive Data

In the Composition API

  • reactive refs
  • reactive objects
  • computed props (kind of 🤪)

3 ways to define reactive data

🤔 How do we type them?

reactive()

Type reactive()

const workshop = reactive({ 
  title: 'TypeScript + Vue.js',
  awesome: true,
  date: new Date(Date.now())
});

supports implicit types

IDE knows that workshop has these properties and they must be of these types (show in IDE)

Type reactive()

interface Workshop{
  title: string;
  awesome: boolean;
  date: Date
}


const workshop: Workshop = reactive({ 
  title: 'TypeScript + Vue.js',
  awesome: true,
  date: new Date(Date.now())
});

Also supports explicit types

ref()

ps. this is my preferred way of declaring reactive data. (I use it exclusively).

Type ref()

const workshop = ref({ 
  title: 'TypeScript + Vue.js',
  awesome: true,
  date: new Date()
});

supports implicit types

IDE knows that workshop has these properties and they must be of these types (show in IDE)

Type ref()

interface Workshop {
  title: string;
  awesome: boolean;
  date: Date;
}

const workshop = ref<Workshop>({
  title: "TypeScript + Vue.js",
  awesome: true,
  date: new Date(),
});

also supports explicit types

Generic arg for ref

Type ref()

interface Workshop {
  title: string;
  awesome: boolean;
  date: Date;
}

const workshop: Ref<Workshop> = ref({
  title: "TypeScript + Vue.js",
  awesome: true,
  date: new Date(),
});

also supports explicit types

Same thing as

Which to use? 🤔

I prefer the generic argument for ref()

const workshop = ref<Workshop>({...});
  • a little bit shorter
  • don't have to import the Ref type from Vue (although in Nuxt it is globally available)
import type { Ref } from 'vue';

Some tips on implicit vs explicit

Some tips on implicit vs explicit

  • prefer implicit when possible
    • Why? It's less verbose
    • An initial value makes this possible
  • explicit is great for:
    • data that is empty and will be filled asynchronously
interface Post {
  title: string;
  author: User;
  body: string;
  publishedAt: Date;
}

const posts = ref<Post[]>([]);

function fetchPosts(){
  // get the posts
  const fetchedPosts = // fetch from API
  // add them to the local data
  posts.value = fetchedPosts;
}

with arrays

not arrays

interface Post {
  title: string;
  author: User;
  body: string;
  publishedAt: Date;
}

const post = ref<Post>();

function fetchPost(){
  // get the posts
  const fetchedPost = // fetch from API
  // set the local data
  post.value = fetchedPost;
}

No default value provided

Ref<Post | undefined>

alternately init to null

interface Post {
  title: string;
  author: User;
  body: string;
  publishedAt: Date;
}

const post = ref<Post | null>(null);

function fetchPost(){
  // get the posts
  const fetchedPost = // fetch from API
  // set the local data
  post.value = fetchedPost;
}

Ref<Post | null>

Some tips on implicit vs explicit

  • prefer implicit when possible
  • explicit is great for:
    • data that is empty and will be filled asynchronously
    • data that can change types 👈

(⚠️ Not usually recommended!)

const postsCount = ref<string | number>(0);

// this will work
postsCount.value = "3"

use the union operator to specify multiple types

const postsCount = ref<any>(0);

// this will work
postsCount.value = "3"

❌ don't use any!

computed()

Type computed()

Is implicitly typed based on return

const a = ref(2);
const b = ref(3);

const sum = computed(()=> a.value + b.value)

ComputedRef<number>

Type computed()

Is implicitly typed based on return

const a = ref(2);
const b = ref(3);

const sum = computed(()=> a.value + b.value)
sum.value 

number

Type computed()

can also be explicit by typing the function return

const a = ref(2);
const b = ref(3);

const sum = computed((): number => a.value + b.value)

Type computed()

can also be explicit by typing the function return

const a = ref(2);
const b = ref(3);

const sum = computed((): number => a.value + b.value)

🤔Why?

When you have longer computed props with multiple conditionals

const myComplexComputedProp = computed((): string =>{
  // lots of conditional logic in here
  // I know I want to end up with a string though
  // Let me explicitly type it so I make sure I 
  //   don't miss returning a string for all cases
})

Options API

Not recommended but possible

import {defineComponent} from "vue"
export default defineComponent({
  //...
})

must define component options with `defineComponent`

export default defineComponent({
  data(){
    return {
      a: 2 // implicitly typed as number
    }
  }
})

data()

export default defineComponent({
  data(){
    return {
      a: 2 as number | string
    }
  }
})

can typecast with the "as" keyword

data()

computed()

export default defineComponent({
  data(){
    return { 
      name: "Daniel",
      a: 1,
      b: 2
    }
  },
  computed:{
    // 👇 implicitly typed as string
    greeting(){ 
      return `Hello ${this.name}` 
    },
    // 👇 explicitly typed as number
    sum(): number{
      return this.a + this.b
    }
  }
})

Questions?

🙋🏾‍♀️🙋

Exercise #2

👩‍💻👨🏽‍💻

That's all for today!

See you tomorrow 👋

Welcome Back!

👋

3

How to Type Component Methods

Composition API

Composition API

Typing component methods are exactly like typing normal TypeScript functions...

Why? Because CAPI methods are normal functions!

Refresher on TS for Functions

function sum(a:number, b:number){
  return a + b;
}

Always type your arguments

function sum(a:number, b:number){
  return a + b;
}

// 👇 type is a number
const total = sum(1, 2) 

return type is implicitly typed

function sum(a:number, b:number): number {
  return a + b;
}

// 👇 type is a number
const total = sum(1, 2) 

can also be explicit

function performSideAffect(): void {
  // do things but don't return
}

functions with no return are implicitly void

(you can explicitly type as void too)

function afterSomething(callback: (b: number) => void) {
  // do the something then...
  callback(2)
}

Can type callback arguments

function asArray<T>(myVar: T): T[] {
  return [myVar];
}

const test = asArray(true) // type is: boolean[]
const test2 = asArray("hello") // type is: string[]
const test3 = asArray<string>("hello again") // explicit

Can use generics for more flexibility

Options API

Options API

All the same rules apply, we're just working with an object's method instead of a straight function

export default defineComponent({
  methods: {
    sum(a: number, b: number): number {
      return a + b;
    },
  },
});

Questions?

🙋🏾‍♀️🙋

Exercise #3

👩‍💻👨🏽‍💻

4

How to Type Component Interfaces

(props and events)

Props

Let's start with

Why Type Props?

Why Type Props?

See required props when using component

Include props in

autocomplete options

Why Type Props?

Red squiggly lines when a prop's value isn't the correct type

Why Type Props?

Hovering tells us what the issue is

Why Type Props?

  • Makes components essentially self documenting
  • Component consumers don't have to jump back and forth between docs or source code
  • Ensures you pass the proper data types to the component

How to Type Props?

with the Composition API

How to Type Props?

Let's start with JS definition and see what changes

defineProps({
  thumbnail: { type: String, default: "noimage.jpg" },
  title: { type: String, required: true },
  description: { type: String, required: true },
  price: { type: Number },
});

(runtime declaration ➡️ type-based declaration)

How to Type Props?

Move the props into a generic

defineProps<{
  thumbnail: { type: String, default: "noimage.jpg" },
  title: { type: String, required: true },
  description: { type: String, required: true },
  price: { type: Number },
}>();

Parens are empty

How to Type Props?

Change runtime definitions to TS definitions

defineProps<{
  thumbnail?: string,
  title: string,
  description: string,
  price: number
}>();

How to Type Props?

Change runtime definitions to TS definitions

defineProps<{
  thumbnail?: string,
  title: string,
  description: string,
  price?: number
}>();

Append optional props with a question mark

Use lowercase types instead of uppercase runtime constructors

How to Type Props?

Define defaults via the `withDefaults` function

withDefaults(defineProps<{
  thumbnail?: string,
  title: string,
  description: string,
  price?: number
}>(), {
  thumbnail: "noimage.jpg"
});

object of defaults as 2nd argument

How to Type Props?

Destructure (3.5+ only)

const {
  thumbnail: "noimage.jpg"
} = defineProps<{
  thumbnail?: string,
  title: string,
  description: string,
  price?: number
}>());

How to Type Props?

Document with JS Docs

withDefaults(defineProps<{
  /** a 16x9 image of the product */
  thumbnail?: string,
  title: string,
  description: string,
  price?: number
}>(), {
  thumbnail: "noimage.jpg"
});

How to Type Props?

Document with JS Docs

shows when hovering over the prop when used

How to Type Props?

Extract to Props Interface (optional)

interface Props {
  /** a 16x9 image of the product */
  thumbnail?: string,
  title: string,
  description: string,
  price?: number
}

defineProps<Props>();

Props are Now type safe Inside and Outside the Component

Other Props Hints

Can use custom interfaces or types

interface Coordinates:{
  x: number,
  y: number,
}
defineProps<{
  coordinates: Coordinates
  //...
}>();

Other Props Hints

Use union types to limit prop options to set of values

defineProps<{
  size: 'sm' | 'md' | 'lg'
}>();

Other Props Hints

Use generics for complex typing scenarios

<script lang="ts" setup generic="T">
interface Props {
  value: T;
  options: T[];
}
const props = defineProps<Props>();
</script>

Only in ^3.3

Other Props Hints

Use generics for complex typing scenarios

With the Options API

Stick with runtime declaration with PropType helper for complex types

import type { PropType } from 'vue'

interface Coordinates{
  x: number,
  y: number
}

defineComponent({
  // type inference enabled
  props: {
    thumbnail: { type: String, default: "noimage.jpg" },
    title: { type: String, required: true },
    description: { type: String, required: true },
    price: { type: Number },
    coordinates: {
      // provide more specific type to `Object`
      type: Object as PropType<Coordinates>,
      required: true
    },
  },
})

Events

What about events?

Why Type Events?

Why Type Events?

Event will show up in autocomplete when you type `@`

Why Type Events?

Event payload is can be typed

How do we type events?

2 different syntaxes

How do we type events?

defineEmits<{
  (e: "myCustomEvent", payload: string): void;
}>();

the original long syntax

How do we type events?

defineEmits<{
    myCustomEvent: [payload: string];
}>();

the new and improved short syntax (^3.3 only)

How do we type events?

💡 Tip: Create a VS Code Snippet

Options API

import { defineComponent } from 'vue'

export default defineComponent({
  emits: {
    myCustomEvent( payload: string ){
      // optionally provide runtime validation 
      return typeof payload === 'string';
      
      // if don't want runtime validation just return true
      // return true
    }
  },
})

Composition API

const emit = defineEmits({
  change: (id: number) => {
    // return `true` or `false` to indicate
    // validation pass / fail
  },
  update: (value: string) => {
    // return `true` or `false` to indicate
    // validation pass / fail
  }
})

Quick Tip! Runtime validation

Questions?

🙋🏾‍♀️🙋

Exercise #4

👩‍💻👨🏽‍💻

5

Other Misc. Types

(template refs, provide inject, slots, composables)

Template Refs

Grant you access to a component's DOM elements

Template Refs

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// type: Ref<HTMLInputElement | null>
const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
  if(!el.value) return;
  el.value.focus()
})
</script>

<template>
  <input ref="el" />
</template>

Template Refs

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// type: Ref<HTMLInputElement | undefined>
const el = ref<HTMLInputElement>()

onMounted(() => {
  if(!el.value) return;
  el.value.focus()
})
</script>

<template>
  <input ref="el" />
</template>

Template Refs

Template Refs

Not sure what DOM element type to use?

Just start typing HTML...

Template Refs

Also sometimes used to access component in parent

(3-TemplateRef.vue example in IDE)

Provide/Inject

Allows you to pass data through multiple nested levels of components without prop drilling

Provide/Inject

Grandfather.vue

Father.vue

Son.vue

GreatGrandfather.vue

GreatGrandfather.vue

GreatGrandmother.vue

Grandfather.vue

Daughter.vue

📀

📀

Provide/Inject

Grandfather.vue

Father.vue

Son.vue

GreatGrandfather.vue

GreatGrandfather.vue

GreatGrandmother.vue

Grandfather.vue

Daughter.vue

📀

📀

App.vue

📀

📀

Provide/Inject

// in some central place for Injection Keys
// @/InjectionKeys.ts ?
// 
import type { InjectionKey } from 'vue'
export const myInjectedKey = Symbol() as InjectionKey<string>

Step #1 - Define a key to identify the injected ata

Define as a symbol

Cast to an InjectionKey

Provide the type that the injected data should be

// In any component
import { provide } from 'vue'
import { myInjectedKey } from "@/InjectionKeys"


provide(myInjectedKey, 'foo')

Provide/Inject

Step #2 - Provide the data using the injection key

providing a non-string value would result in an error

// in any component nested below the previous
import { inject } from 'vue'
import { myInjectedKey } from "@/InjectionKeys"

const foo = inject(myInjectedKey) // "foo"

Provide/Inject

Step #3 - Access the data with inject() in child, grandchild, etc

will be string | undefined
(possibly undefined if not provided higher up the tree)

Provide/Inject Hints

You can provide reactive data, just make sure to type it correctly

import type { InjectionKey } from "vue";
const Key = Symbol() as InjectionKey<Ref<string>>;

const data = ref("");
provide(Key, data);

Define the reactive data and pass to provide

Use the Ref generic when typing key

Provide/Inject Hints

Provide/Inject is also really good at providing data between tightly coupled components

<Accordian>
  <AccordianPanel title="First Panel"> 
    Hello First Panel 
  </AccordianPanel>
  <AccordianPanel title="Second Panel"> 
    Hello Second Panel 
  </AccordianPanel>
  <AccordianPanel title="Third Panel"> 
    Hello Third Panel 
  </AccordianPanel>
</Accordian>

Provide/Inject Hints

You can define the key in the same component you provide the data

// Accordian.vue
<script lang="ts">
import type { InjectionKey } from "vue";
export const AppAccoridanKey = Symbol() as InjectionKey<Ref<string>>;
</script>

<script setup lang="ts">
const activePanel = ref("");
provide(AppAccoridanKey, activePanel);
</script>

Define it in another script section WITHOUT setup

Composables

How do we type composables?

Composables

It's really easy, so long as you type all your reactive data and methods correctly.

(example in IDE - compables/useCounter.ts)

Composables Tip

MaybeRefOrGetter Type

export const useFetch = (url: MaybeRefOrGetter) => {
  //...
};

// Non-reactive value
useFetch("https://vueschool.io")

// Reactive value (ref or computed)
const url = ref("https://vueschool.io")
useFetch(url)

// Getter
useFetch(()=> "https://vueschool.io")



A Composable in the Wild

VueUse useNow()

Welcome back!

👋

Slots

Slots are implicitly typed out of the box

<!--UserCard.vue-->
<script setup lang="ts">
  
const user = ref({
  name: 'John Doe',
  email: 'test@test.com',
})
</script> 

<template>
  <slot name="header" :user="user">
    <h1>
      {{user.name}}
    </h1>
  </slot>
</template>

Given this component definition

This usage is implicitly typed correctly

// UserCard.vue

defineSlots<{
  header: (props: {
    user: {
      name: string | number;
      email: string;
    };
  }) => any;
}>();

Can define slot types explicitly with defineSlots

Results in:

Questions?

🙋🏾‍♀️🙋

Exercise #5

👩‍💻👨🏽‍💻

Exercise #6

👩‍💻👨🏽‍💻

7

Advanced TypeScript Tips

Type Re-Usability And Organization

Let's talk about

Type Re-Usability And Organization

Create a Central Types folder for Re-Usable Types

// @/types

Type Re-Usability And Organization

Create Domain-Specific Type Files

within the types directory

// @/types/auth.ts
export interface Credentials {
  username: string
  password: string
}

export interface AuthState {
  user: User | null
  isAuthenticated: boolean
  token: string | null
}

Type Re-Usability And Organization

Use Barrel Files for Cleaner Imports 

// @/types/index.ts
export * from './auth'
export * from './product'
export * from './order'

// in component all are imported from @/types
import { User, Product, Order } from '@/types'

Type Re-Usability And Organization

Use Type Composition and Inheritance

for More Maintainable and Consistent Types

interface BaseEntity {
  id: number
  createdAt: Date
  updatedAt: Date
}

// Specific entity types
interface User extends BaseEntity {
  name: string
  email: string
}

interface Product extends BaseEntity {
  name: string
  price: number
}

Type Re-Usability And Organization

Implement Generic Types for Common Patterns

// Pagination response type
interface PaginatedResponse<T> {
  data: T[]
  total: number
  page: number
  pageSize: number
}

// API response wrapper
type ApiResponse<T> = {
  data: T
  status: number
  message: string
}

Type Re-Usability And Organization

Declare commonly used types in the

global scope to skip importing

// @/types/global.ts
declare global {
	interface PaginatedResponse<T> {...}

	type ApiResponse<T> = {...}
  	//... all global types here
}

export {};

(don't dump everything here, can cause conflicts and make type definitions harder to track down)

💡

must export something!

Type Re-Usability And Organization

Declare namespaced types global scope

to skip importing and limit conflicts

// @/types/global.ts
declare global {
  	namespace App{
    	interface PaginatedResponse<T> {...}
		type ApiResponse<T> = {...}
    }
}

export {};

Type Re-Usability And Organization

Global types in namespace also enhance discoverability

Built-in Utility Types?

What about

Built-in Utility Types

interface User {
  id: number;
  name: string;
  email: string;
}

// All fields are optional
type UserUpdate = Partial<User>;

// Function that accepts partial updates
function updateUser(id: number, updates: Partial<User>) {
  // Implementation
}

Partial<T>

Makes all object properties optional

Built-in Utility Types

interface Config {
  apiUrl?: string;
  timeout?: number;
}

// All fields are required
type RequiredConfig = Required<Config>;

Required<T>

Does the opposite of Partial<T>

Built-in Utility Types

interface User {
  id: number;
  name: string;
  email: string;
}

// Just name and email
type UserCredentials = 
	Pick<User, 'name' | 'email'>;

// { name: string; email: string }

Pick<T, K>

Selects a subset of Properties

Built-in Utility Types

interface User {
  id: number;
  name: string;
  email: string;
}

// Everything except id
type UserWithoutId = Omit<User, 'id'>;

// { name: string; email: string }

Omit<T, K>

Excludes specific properties

Built-in Utility Types

// Dictionary of users by ID
type UserDictionary = Record<string, User>;

// User permissions mapping
type UserPermissions = Record<string, boolean>;

Record<T, K>

creates an object type with keys of type K and values of type T

Built-in Utility Types

function fetchUser(id: number) {
  return { 
    id,
	name: 'User', 
    email: 'user@example.com' 
  };
}


type FetchResult = ReturnType<typeof fetchUser>;
// { id: number; name: string; email: string; }

ReturnType<T>

extracts the return type of a function

Built-in Utility Types

function fetchUser(id: number) {
  return { 
    id,
	name: 'User', 
    email: 'user@example.com' 
  };
}

type FetchParams = Parameters<typeof fetchUser>;
// [id: number]

Parameters<T>

extracts parameter types as tuple

Built-in Utility Types

type Status = 
	'pending' | 'approved' | 'rejected';


// 'approved' | 'rejected'
type FinalStatus 
	= Extract<Status, 'approved' | 'rejected'>;

Extract<T, U>

extracts types from T that are assignable to U

Built-in Utility Types

type Status = 
	'pending' | 'approved' | 'rejected';


// 'pending'
type NonFinalStatus = 
	Exclude<Status, 'approved' | 'rejected'>;

Exclude<T, U>

excludes types from T that are assignable to U

Built-in Utility Types

// Config that shouldn't be modified after initialization
interface AppConfig {
  apiUrl: string;
  maxRetries: number;
  timeout: number;
}

// Make it readonly
const config: Readonly<AppConfig> = {
  apiUrl: 'https://api.example.com',
  maxRetries: 3,
  timeout: 5000
};

// Error: Cannot assign to 'timeout' because it is a read-only property
// config.timeout = 10000; 

Readonly<T>

assigns variable as readonly (deep)

Built-in Utility Types

type UserResponse = User | null | undefined;

// Removes null and undefined
type ValidUser = NonNullable<UserResponse>;

function getUser(): ValidUser{
	// getUser from API...
  const user = UserResponse;
  if (user) return user;
  throw new Error("No active user");
}

NonNullable<T>

Removes null and undefined from a type

Built-in Utility Types

async function fetchUser(): Promise<User> {
  return user;
}

// Extracts User from Promise<User>
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;

Awaited<T>

Unwraps the type from a Promise

Some Useful TS Operators

(Not really utility types but extremely useful)

interface User {
  id: number;
  name: string;
  email: string;
}

// 'id' | 'name' | 'email'
type UserKeys = keyof User;

keyof

get keys of object as union type

Some Useful TS Operators

Some Useful TS Operators

const defaultTheme = {
  primaryColor: '#007bff',
  secondaryColor: '#6c757d',
};

// { primaryColor: string; secondaryColor: string }
type Theme = typeof defaultTheme;

// Create a new theme with the same type structure
const darkTheme: Theme = {
  primaryColor: '#343a40',
  secondaryColor: '#495057',
};

typeof

infer type from runtime definition

Some Useful TS Operators

type sizes = "sm" | "md" | "lg";

// text-sm, text-md, text-lg
type SizeClasses = `text-${sizes}`;

// { lg: "text-lg", md: "text-md", sm: "text-sm" }
type sizeClassMap = {
  [k in sizes]: `text-${k}`;
};

in

creates mapped types by iterating over a union of keys

Some Useful TS Operators

type EntityService<T> = {
  [K in 'create' | 'read' | 'update' | 'delete']: (data: T) => Promise<void>
};

// Usage
interface Product {
  id: string;
  name: string;
  price: number;
}

// Creates functions for CRUD operations
const productService: EntityService<Product> = {
  create: async (product) => {},
  read: async (product) => {},
  update: async (product) => {},
  delete: async (product) => {}
};

in

another practical example

Built-in Utility Types

type UserForm = {
  name: string;
  email: string;
  password: string;
};


type UserFormErrors = Partial<Record<keyof UserForm, string>>;

const userForm = ref<UserForm>({
  name: "",
  email: "",
  password: "",
});

const userFormErrors = computed((): UserFormErrors => {
  const errors: Partial<UserFormErrors> = {};
  if (!userForm.value.name) errors.name = "Name is required";
  if (!userForm.value.email) errors.email = "Email is required";
  if (!userForm.value.password) errors.password = "Password is required";
  return errors;
});

Combining Utilities for Common Use Cases

Form States

You can even write custom utilities

(they're just generic types) !

We've already seen some 👇

interface PaginatedResponse<T> {
  data: T[]
  total: number
  page: number
  pageSize: number
}

type ApiResponse<T> = {
  data: T
  status: number
  message: string
}

(these are fairly opinionated)

There are also many 3rd party utilities

  • TS Toolbelt - TypeScript's largest utility library
  • Type Fest - A collection of essential TypeScript types
  • TS Utils - A bunch of common utilities in pure Typescript to deal with primitives, strings, urls, objects, collections, dates etc.

Let's Look at TS Toolbelt

TypeScript Hints Inspired from Vue Source Code

use const assertions for implicit types that assume NO change

Use Type Assertion Functions for type-safe config

Just returns the config

Use Type Assertion Functions for type-safe config

Typesafe Global State Management with Pinia

Zod

A runtime library for declaring types

Use Cases for Zod

  • Type Your Project Resource/Entities (Users, Posts, etc)
  • Use as runtime validator for client side form validation
  • Use as runtime validator for API endpoints
  • Validate environment variables at runtime
  • Validate query string parameters at runtime

Vue Form Libraries that Support Zod:

  • FormKit
  • Formwerk (from creator of Vee Validate)
  • Vee-Validate

Type Narrowing

The proces of a variable becomeing more specific within a certain code block based on type checks or assertions

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this block, TypeScript knows that id is a string
    console.log(id.toUpperCase());
  } else {
    // In this block, TypeScript knows that id is a number
    console.log(id.toFixed(2));
  }
}

Type Narrowing with typeof

check if object is a particular type

try{
  riskyOperationThatCouldThrowDifferentErrorTypes()
}catch(error){

  	if(error instanceof CustomError){
      
    }
	if(error instanceof Error){
      
    }
  
	// all esle       
}
 

Type Narrowing with instanceof

check if object extends a particular class

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}

Narrowing with the `in` operator

check if property/method exists on an object

interface Square {
  kind: "square";
  size: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

type Shape = Square | Rectangle;

// This is a type predicate function
function isSquare(shape: Shape): shape is Square {
  return shape.kind === "square";
}

function calculateArea(shape: Shape) {
  if (isSquare(shape)) {
    // TypeScript knows shape is Square here
    return shape.size * shape.size;
  } else {
    // TypeScript knows shape is Rectangle here
    return shape.width * shape.height;
  }
}

Type Narrowing with Type Predicate Functions

(boolean functions that narrow)

Final Questions?

🙋🏾‍♀️🙋

Ask me anything? 😀

Vue School Courses

BTW, most of our courses are also TypeScript first

Workshops for your company

team@vueschool.io

Thank you

🙏

TypeScript + Vue Workshop

By Daniel Kelly

TypeScript + Vue Workshop

  • 1,147