Subscribe on changes!

Computed properties don't work with TypeScript type predicates properly

avatar
Mar 9th 2022

Version

3.2.31

Reproduction link

stackblitz.com

Steps to reproduce

To reproduce from the minimal reproduction link:

  1. Open https://stackblitz.com/edit/vitejs-vite-aueg8h?file=src/App.vue&view=editor
  2. Run npm run typecheck in the terminal.

To reproduce from scratch:

  1. Create a new component that uses TypeScript.
  2. Create a user-defined type guard function (type predicates documentation: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates).
  3. Create a ref that can be used as input to the previously created function.
  4. Create a computed that returns the value of calling the function with the ref.
  5. Create an if statement, in the script tag or the template tag, that uses the computed, and inside the if, access a property of the ref that is only present in one of its union types.
  6. Run vue-tsc to type check the component.

What is expected?

No errors appear in the terminal. Also, if a linter is configured, it doesn't show errors.

What is actually happening?

Typescript error TS2339 appear in the terminal:

error TS2339: Property 'swim' does not exist on type '{ swim: string; } | { fly: string; }'.
  Property 'swim' does not exist on type '{ fly: string; }'.

Even though calling the type guard function directly doesn't raise an error, it should also work with computed properties to allow reusable code.

Code

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

type Fish = { swim: string };
type Bird = { fly: string };
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

const pet = ref<Fish | Bird>({ swim: '💦' });

const petIsFish = computed<boolean>(() => {
  return isFish(pet.value);
});

// ----------------------------------------------------------------------------

// Type error
if (petIsFish.value) {
  pet.value.swim;
}

// No type error
if (isFish(pet.value)) {
  pet.value.swim;
}

// ----------------------------------------------------------------------------
// TypeScript pure example:

const pet2: Fish | Bird = { swim: '💦' } as Fish | Bird;
const pet2IsFish = isFish(pet2);

// No type error
if (pet2IsFish) {
  pet2.swim;
}
// No type error
if (isFish(pet2)) {
  pet2.swim;
}
</script>

<template>
  <!-- Type error -->
  <div v-if="petIsFish">{{ pet.swim }}</div>

  <!-- No type error -->
  <div v-if="isFish(pet)">{{ pet.swim }}</div>
</template>
avatar
Mar 9th 2022

That's simply a Typescript limitation, i would say.

I tried to come up with something comparable in plain Typescript, see this playground

avatar
Mar 11th 2022

@pikax Thoughts? Am I right? Is this a limitation in TS that we can't resolve?

avatar
Mar 11th 2022

@LinusBorg

Is a bit different, when you are using a computed it will be passed as a callback (altho in other cases it doesn't - issue with self accessing computed).

Playground using actual computed

Simple example showing that is typescript limitation on callback - Playground:

const pet2: Fish | Bird = { swim: '💦' } as Fish | Bird;

declare function method<T>(
  getter: () => T
): T

const petIsFish3= method(()=> isFish(pet2))
// Type error
if (petIsFish3) {
  pet2.swim;
}