Workshop
+ Vue.js 3
1
Intro to TypeScript with Vue.js
Why Use TypeScript with Vue.js?
1
1
1
🤔 Must be something to TypeScript
2
2
Debugging in plain JS takes running code
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
Can you spot the error?
2
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
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>
3
3
Websites and apps are ever evolving projects.
3
examples/1-RefactorExample.vue
3
BTW: There are many tools for generating types from Database schema
4
4
examples/2-AutocompleteExample.vue
Setup a Vue Project for TypeScript
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
Now you can use TypeScript!
<!--App.vue-->
<!-- with the Composition API (👉 recommended)-->
<script setup lang="ts"></script>
<!-- with the composition API -->
<script lang="ts"></script>
For VS Code
(Webstorm provides support out of the box)
Step #1: Install Vue Language Features (previously Volar)
Step #2: No step 2, that's it
🎉
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
3 ways to define reactive data
🤔 How do we type them?
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)
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
ps. this is my preferred way of declaring reactive data. (I use it exclusively).
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)
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
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
I prefer the generic argument for ref()
const workshop = ref<Workshop>({...});
import type { Ref } from 'vue';
Some tips on implicit vs explicit
Some tips on implicit vs explicit
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
(⚠️ 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!
Is implicitly typed based on return
const a = ref(2);
const b = ref(3);
const sum = computed(()=> a.value + b.value)
ComputedRef<number>
Is implicitly typed based on return
const a = ref(2);
const b = ref(3);
const sum = computed(()=> a.value + b.value)
sum.value
number
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)
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
})
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
}
}
})
export default defineComponent({
data(){
return {
a: 2 as number | string
}
}
})
can typecast with the "as" keyword
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
Typing component methods are exactly like typing normal TypeScript functions...
Why? Because CAPI methods are normal 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
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)
Let's start with
See required props when using component
Include props in
autocomplete options
Red squiggly lines when a prop's value isn't the correct type
Hovering tells us what the issue is
with the Composition API
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)
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
Change runtime definitions to TS definitions
defineProps<{
thumbnail?: string,
title: string,
description: string,
price: number
}>();
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
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
Destructure (3.5+ only)
const {
thumbnail: "noimage.jpg"
} = defineProps<{
thumbnail?: string,
title: string,
description: string,
price?: number
}>());
Document with JS Docs
withDefaults(defineProps<{
/** a 16x9 image of the product */
thumbnail?: string,
title: string,
description: string,
price?: number
}>(), {
thumbnail: "noimage.jpg"
});
Document with JS Docs
shows when hovering over the prop when used
Extract to Props Interface (optional)
interface Props {
/** a 16x9 image of the product */
thumbnail?: string,
title: string,
description: string,
price?: number
}
defineProps<Props>();
Can use custom interfaces or types
interface Coordinates:{
x: number,
y: number,
}
defineProps<{
coordinates: Coordinates
//...
}>();
Use union types to limit prop options to set of values
defineProps<{
size: 'sm' | 'md' | 'lg'
}>();
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
Use generics for complex typing scenarios
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
},
},
})
What about events?
Event will show up in autocomplete when you type `@`
Event payload is can be typed
2 different syntaxes
defineEmits<{
(e: "myCustomEvent", payload: string): void;
}>();
the original long syntax
defineEmits<{
myCustomEvent: [payload: string];
}>();
the new and improved short syntax (^3.3 only)
💡 Tip: Create a VS Code Snippet
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
}
},
})
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)
Grant you access to a component's DOM elements
<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>
<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>
Not sure what DOM element type to use?
Just start typing HTML...
Also sometimes used to access component in parent
(3-TemplateRef.vue example in IDE)
Allows you to pass data through multiple nested levels of components without prop drilling
Grandfather.vue
Father.vue
Son.vue
GreatGrandfather.vue
GreatGrandfather.vue
GreatGrandmother.vue
Grandfather.vue
Daughter.vue
📀
📀
❌
Grandfather.vue
Father.vue
Son.vue
GreatGrandfather.vue
GreatGrandfather.vue
GreatGrandmother.vue
Grandfather.vue
Daughter.vue
📀
📀
App.vue
📀
📀
// 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')
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"
Step #3 - Access the data with inject() in child, grandchild, etc
will be string | undefined
(possibly undefined if not provided higher up the tree)
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 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>
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
How do we type composables?
It's really easy, so long as you type all your reactive data and methods correctly.
(example in IDE - compables/useCounter.ts)
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")
VueUse useNow()
Questions?
🙋🏾♀️🙋
Exercise #5
👩💻👨🏽💻
Exercise #6
👩💻👨🏽💻
Final Questions?
🙋🏾♀️🙋
Ask me anything? 😀
Vue School Courses
BTW, most of our courses are also TypeScript first
team@vueschool.io
Thank you
🙏