1

Noob with TS here. I am creating a custom Icon component in React and I want to only use Rounded Material UI icons.

I am defining the type like so:

import * as icons from '@material-ui/icons/index'
export type MaterialUiIcon = keyof typeof icons

this works but only gives me typechecking for all material UI icons.

instead this is what I want to do:

const roundedIconsNames = (Object.keys(icons).filter((icon) => icon.includes('Rounded')))
export type RoundedMaterialUiIcon = typeof roundedIconsNames[number]

however RoundedMaterialUiIcon ends up being type string[]

How can I make this work?

Thank you.

1 Answer 1

1

You are receiving string[] because Object.keys always returns string[] by design.

I believe it is better to make typeguard and utility function:

import * as icons from '@material-ui/icons/index'
type Icons = typeof icons

export type MaterialUiIcon = keyof Icons

type Prefix<T extends string> = `${string}${T}` | `${string}${T}${string}` | `${T}${string}`

// self explanatory, we have 3 variant of word
type Test1 = Prefix<'Rounded'> // `${string}Rounded` | `${string}Rounded${string}` | `Rounded${string}`


type GetByPrefix<T, P> = T extends P ? T : never;

/**
 * This will return only HelloRounded, because union 'HelloRounded' | 'Batman'
 * extends `${string}Rounded` | `${string}Rounded${string}` | `Rounded${string}`
 * 
 * WHy extends?
 * Because Rounded is at the end
 */
type Test2 = GetByPrefix<'HelloRounded' | 'Batman', Prefix<'Rounded'>> // 'HelloRounded'

/**
 * This will return "RoundedWorld" because Rounded is at the beginning
 */
type Test3 = GetByPrefix<'RoundedWorld' | 'Batman', Prefix<'Rounded'>> // "RoundedWorld"

/**
 * This is a special syntax for user defined typeguards
 * It may help you to narrow the type
 * https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
 */
const typeguard = <T extends string,>(tag: string) => (icon: MaterialUiIcon): icon is GetByPrefix<MaterialUiIcon, Prefix<T>> => icon.includes(tag)

const getRounded = <T extends string>(icons: Icons, includes: T) =>
/**
 * Object.keys will always return string[], this is by design
 * So you need to use type assertion here
 * 
 * Array.prototype.filter accept curried typeguard
 * It is a good practive yo use user defined typeguards a a predicate
 * callback in array methods
 */
    (Object.keys(icons) as Array<MaterialUiIcon>).filter(typeguard<T>(includes))

const result = getRounded(icons, 'Rounded')


Pls, keep in mind, since Icons have 5K union types, it is not easy task for TS to iterate through all unions.

Playground

Sign up to request clarification or add additional context in comments.

2 Comments

this is amazing - I am still trying to understand what's happening but thank you so much for the solution!
give me a moment, I will write detailed explanation

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.