Subscribe on changes!

How to make shared props type-safe with defineComponent

avatar
Apr 12th 2022

Version

3.2.31

Reproduction link

stackblitz.com

unknown-property

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>>> & {}>>'.(2339)

Please see line 66 of file MyText.ts

avatar
Apr 12th 2022

When you remove the generic inputs for defineComponent() and instead let Typescript infer them as intended, it works for me:

image

Is that what you are talking about? your description is quite brief and not giving much context of the use case.

avatar
Apr 12th 2022

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

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

avatar
Apr 12th 2022

I have a hard time reproducing that. Downloaded the repro to run it locally, still this is what I see:

image

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>
}
avatar
Apr 12th 2022

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 🗡️

avatar
Apr 12th 2022

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?

avatar
Apr 12th 2022

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
    }
})

playground

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 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'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
    }
})

playground

avatar
Apr 12th 2022

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 👍

avatar
Apr 12th 2022

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>
avatar
Apr 12th 2022

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 if ExtractPropTypes<typeof TypoProps> would then also take the 'required' in consideration. and yes it does :)