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(Date.now())
});

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(Date.now()),
});

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(Date.now()),
});

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?

Don't use this deprecated syntax from reactivity transform

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

  • 660