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
👩💻👨🏽💻
Coffee Break
☕️
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, and 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()
Questions?
🙋🏾♀️🙋
Exercise #5
👩💻👨🏽💻
Exercise #6
👩💻👨🏽💻
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
- 999