Igor Sukharev
Igor Sukharev

Reputation: 3118

How to define object's property type depending on a key name

I want to achieve:

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?

Upvotes: 1

Views: 4253

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250366

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

Upvotes: 2

Related Questions