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