Computed properties don't work with TypeScript type predicates properly
Version
3.2.31
Reproduction link
Steps to reproduce
To reproduce from the minimal reproduction link:
- Open https://stackblitz.com/edit/vitejs-vite-aueg8h?file=src/App.vue&view=editor
- Run
npm run typecheck
in the terminal.
To reproduce from scratch:
- Create a new component that uses TypeScript.
- Create a user-defined type guard function (type predicates documentation: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates).
- Create a
ref
that can be used as input to the previously created function. - Create a
computed
that returns the value of calling the function with the ref. - 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.
- 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>
That's simply a Typescript limitation, i would say.
I tried to come up with something comparable in plain Typescript, see this playground
@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;
}