How to make shared props type-safe with defineComponent
Version
3.2.31
Reproduction link
Steps to reproduce
As you can see on stackblitz
What is expected?
I would expect that property 'as' exists on that type, and that no typescript error is displayed
What is actually happening?
Property 'as' does not exist on type 'Readonly<LooseRequired<Readonly<ExtractPropTypes<Readonly<ComponentPropsOptions
Please see line 66 of file MyText.ts
When you remove the generic inputs for defineComponent()
and instead let Typescript infer them as intended, it works for me:
Is that what you are talking about? your description is quite brief and not giving much context of the use case.
Hi @LinusBorg
sorry I tought the use case was obvious. The intention is to be able to share prop definitions accross the application. For example, every typography component such as paragraph, headline, lists etc. could extend the TypoPropsInterface
interface to gurantee that those properties exist. The predefined typograhpy props could then be reused as in line 43. Components that share the same props could be updated in a single point rather than in every component definition directly. This would make the application more maintainable.
When removing the generics, I'd run into the following issue:
TS2345: Argument of type 'Prop<"center" | "left" | "right", "center" | "left" | "right"> | null | undefined' is not assignable to parameter of type '"center" | "left" | "right"'. Type 'undefined' is not assignable to type '"center" | "left" | "right"'.
Of course that could be solved by adding type assertion
console.log(TypoAlignConst.includes(props.align as TypoAlignType));
but as you can see in lines 120 and 121 in the screenshot, it should generally work with typescript.
Update: It seems to be wrapped in the Prop
type, but I'm wondering if this should not be unwrapped inside the setup func
I have a hard time reproducing that. Downloaded the repro to run it locally, still this is what I see:
What version of TS are you using in your real project? I used 4.6 included in VSCode, and improvised a quick tsconfig.json, as the reproduction didn't include TS
Sidenote: I'm actually suprised it works with this:
{
align: String as () => TypoAlignType
}
usually we use PropType
:
{
align: String as PropType<TypoAlignType>
}
I created a new stackblitz and added my tsconfig. When turning strict off, then it works (but also not the ideal solution). stackblitz.com
On my local machine I created the new project by running npm init vue@latest
so basically the standard vite setup with a view modifications.
Sidenote: Hehe
// since
export declare type PropType<T> = PropConstructor<T> | PropConstructor<T>[];
declare type PropConstructor<T = any> = {
new (...args: any[]): T & {};
} | {
(): T; // Here is the reason why it works
} | PropMethod<T>;
// This is basically the same as (): T;
{
align: String as () => TypoAlignType
}
Ofc once the PropType definition would change, I'd be screwed 🗡️
Thanks. Now I can reproduce it.
So it seems as it stands,ComponentPropsOptions<>
can't be used in that way. removing these annotations and adding a as const
on the whole props objects makes it work,...
export const TypoProps = {
align: {
type: String as PropType<TypoAlignType>,
required: true,
},
} as const;
but means your props objects no longer will be checked so writing e.g. require
instead of required
wouldn't go unnoticed, and you want TS to inform you of that mistake.
That about it?
Never have been come across that problem as I'm not yet on the "everything has to be 100% typed everywhere" train. I shared props and was fine with them being plain objects.
Lighting up the batsign for @pikax, master of Vue 3 type magic - any ideas? Is there a more appropriate type we can provide?
Thanks @LinusBorg for the summoning 🧙♂️
Had a quick look on the stackblitz, props should be type safe if declared as props:
defineComponent({
props: {
align: {
type: String as PropType<TypoAlignType>,
required: true,
}
},
setup(props) {
TypoAlignConst.indexOf(props.align) // works
}
})
Using internal interfaces is not recommended:
export const TypoProps: ComponentPropsOptions<TypoPropsInterface> = {
align: String as () => TypoAlignType,
};
That code is actually not current because the runtime prop is actually not-required but you are expecting it to be required, this is quite a bad practice.
The best way to guarantee the type is to make props as const, like @LinusBorg suggested
So it seems as it stands,
ComponentPropsOptions<>
can't be used in that way. removing these annotations and adding aas const
on the whole props objects makes it work,...export const TypoProps = { align: { type: String as PropType<TypoAlignType>, required: true, }, } as const;
but means your props objects no longer will be checked so writing e.g.
require
instead ofrequired
wouldn't go unnoticed, and you want TS to inform you of that mistake.
That's a good spot, you can circumvent that by using a function:
import { PropType, defineComponent, ComponentPropsOptions } from 'vue';
function buildProps<T extends ComponentPropsOptions>(props: T) {
return props
}
export const TypoAlignConst = <const>['center', 'left', 'right'];
export type TypoAlignType = typeof TypoAlignConst[number];
export const TypoProps = buildProps({
align: {
type: String as PropType<TypoAlignType>,
required: true,
},
});
defineComponent({
props: TypoProps,
setup(props) {
TypoAlignConst.indexOf(props.align) // works
}
})
Thank you for the feedback @LinusBorg and @pikax - I was also not quite happy to use ComponentPropsOptions
but it was the closest solution I found.
the buildProps
function looks great, and I guess the fowlling should also work then:
export function useTypoCss(props: ExtractPropTypes<typeof TypoProps>) {
function getCssClasses() {
TypoAlignConst.indexOf(props.align);
}
// ....
}
Thanks for teaching me senpai :)
PS: I really love Vue, keep on the great work 👍
the
buildProps
function looks great and I guess this should also work then:export function useTypoCss(props: ExtractPropTypes<typeof TypoProps>) { function getCssClasses() { TypoAlignConst.indexOf(props.align); } // .... }
I wouldn't do that for useTypoCss
composable, I wouldn't recommend relying on props type... that case it should be explicit type:
function useTypoCss(props: { align: TypoAlignType}) {
// ....
}
The reason for this is better, is because you don't rely on properties, allowing you to use this outside of the component:
useTypoCss({
align: 'right'
})
A closer look at the implementation of useTypoCss
and using props
is not correct!
To make the best of your composable I would rely on vue reactivity:
function useTypoCss(align: MaybeRef<TypoAlignType>) {
function getTextAlignClass(): string | undefined {
const a = unref(align)
if (!a) {
return;
}
return textAlignClassMap[a];
}
return computed(()=> ['some-general-class', getTextAlignClass()]);
}
// usage
const typoAlignClass = useTypoCss(toRef(props, 'align')
<template>
<div :class="typoAlignClass"/>
</template>
I wouldn't do that for
useTypoCss
composable, I wouldn't recommend relying on props type... that case it should be explicit type:function useTypoCss(props: { align: TypoAlignType}) { // .... }
I totally agree,
useTypoCss
was just the only example I had in my code and I used it or the sake of simplicity :D What I wanted to ask was ifExtractPropTypes<typeof TypoProps>
would then also take the 'required' in consideration. and yes it does :)