1

I want to achieve:

  • if property name starts with on, it's type must be a Function
  • in all other cases the type is boolean

My first naive approach was:

interface Props {
  [key: `on${string}`]: Function; // error: `on${string}`' index type 'Function' is not assignable to 'string' index type 'boolean'.ts(2413)
  [key: string]: boolean;
}

My second try was:

type EventName = `on${string}`;

interface Props {
  [K: EventName | string]: typeof K extends EventName ? Function : boolean;
}

const props: Props = {
  asd: true,
  onClick: () => {}, // error: Type '() => void' is not assignable to type 'boolean'.ts(2322)
};

So is it possible to achieve this, maybe in a different way?

Igor Sukharev
  • 2,467
  • 24
  • 21

1 Answers1

2

This isn't 100% possible, since the code couldn't be fully checked. If s contains a string there is no real way to prevent it from containing a string starting with on. This is why the string index signature needs to be compatible with all defined props.

let s = "onClick" as string;
let v = props[s] // typed as boolean, is actually onClick

One version to get around this warning (although not to make it safe) is to use an intersection instead of an interface. This will allow access to properties to work as you want them to, but creation of such objects requires a type assertion, as the underlying incompatibility is still there

type Props = {
  [key: `on${string}`]: Function; 
} & {
  [key: string]: boolean;
}

const props: Props = {
  asd: true,
  onClick: () => {}, 
} as any as Props;

props.onClick // Function
props.ssss // boolean

Playground Link

For creation, to avoid the any type assertio, and get some validation for object creation, we could use a function that validates that any keys not prefixed with on are of type boolean:

function makeProps<T extends Record<`on${string}`, Function>>(o: T & Record<Exclude<keyof T, `on${string}`>, boolean>) {
    return o as Props;
}
const props: Props = makeProps({
  asd: true,
  onClick: () => {}, 
})

Playground Link

The safer solution would be to make the boolean and the Function keys disjoint by using a prefix for the boolean props as well.

type Props = {
  [key: `on${string}`]: (...a: any[]) => any; 
  [key: `flag${string}`]: boolean;
}

const props: Props = {
  onClick: () => {}, 
  flagRaiseClick: true
}

props.onClick // Function
props.flagRaiseClick // boolean
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Hmm, I definitely need the safety. The last solution is closest to what I need, but without the second prefix) I thought the typechecker could test each case in order they appear, so the top one evaluates first and checks whether string starts with `on`. – Igor Sukharev Feb 14 '22 at 16:10