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.
- Business requirements will change
- Scope will grow
- 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