2

This code snippet with Generics works perfectly fine (Link to Simple and Working Code)

const state: Record<string, any> = {
    isPending: false,
    results: ['a', 'b', 'c']
}

const useValue = <T extends {}>(name: string): [T, Function] => {
    const value: T = state[name]
    const setValue: Function = (value: T): void => {
        state[name] = value
    }
    return [value, setValue]
}

const [isPending, setIsPending] = useValue<boolean>('isPending')
const [results, setResults] = useValue<string[]>('results')

Here i can be sure that isPending is a boolean and setIsPending receives a boolean as parameter. Same applies to results and setResults as an array of strings.

Then I wrap the code with another method useStore (Link to Extended Broken Code)

interface UseStore {
    state: Record<string, any>
    useValue: Function
}

const state: Record<string, any> = {
    isPending: false,
    results: ['a', 'b', 'c']
} 

const useStore = (): UseStore => {
    const useValue = <T extends {}>(name: string): [T, Function] => {
        const value: T = state[name]
        const setValue: Function = (value: T): void => {
            state[name] = value
        }
        return [value, setValue]
    }

    return { state, useValue }
}

const { useValue } = useStore()
const [isPending, setIsPending] = useValue<boolean>('isPending')
const [results, setResults] = useValue<string[]>('results')

The last two lines give me typescript errors: Untyped function calls may not accept type arguments.

I suspect the useStore interface to be problematic, but due to the dynamic nature of it i can't think of a better solution.

How can I get rid of the errors while using the Generic type to get proper type hints and code completion?

1 Answer 1

2

Since the type of useValue is Function it makes no sense to pass in generic type arguments. Who do they benefit? The runtime doesn't get them, they are erased at compiler time. The compiler can't use them since Function is just an un-typed function, so there is no benefit there. Passing type arguments is useless and arguably a mistake (ie the user was not expecting this to be Function and is passing in the type arguments thinking they have some effect).

Remove the type arguments and drop the pretense that this is in any way typed:

const { useValue } = useStore()
const [isPending, setIsPending] = useValue('isPending')
const [results, setResults] = useValue('results')

The more interesting question is why are you writing the code like this since there is a way to fully type everything in this code:

const state = {
    isPending: false,
    results: ['a', 'b', 'c']
}
type State = typeof state;

const useStore = () => {
    const useValue = <K extends keyof State>(name: K) => {
        const value = state[name]
        const setValue = (value: State[K]): void => {
            state[name] = value
        }
        return [value, setValue] as const
    }

    return { state, useValue }
}
type UseStore = ReturnType<typeof useStore>;

const { useValue } = useStore()
const [isPending, setIsPending] = useValue('isPending')
const [results, setResults] = useValue('results')

The version above is fully type safe and does not require any duplication of names or types (You could of course split this out in multiple files, but then probably some duplication would need to occur depending on your requirements). If this is not applicable in your case, I would be interested to know why.

Edit

If you just want the types to work out on the last lines and have some type safety there, you just need to specify the signature for the functions using generics:

interface UseStore {
    state: Record<string, any>
    useValue: <T,>(name: string) => [T, (value: T)=> void]
}

const state: Record<string, any> = {
    isPending: false,
    results: ['a', 'b', 'c']
}

const useStore = (): UseStore => {
    const useValue = <T,>(name: string): [T, (value: T)=> void] => {
        const value: T = state[name]
        const setValue = (value: T): void => {
            state[name] = value
        }
        return [value, setValue]
    }

    return { state, useValue }
}

const { useValue } = useStore()
const [isPending, setIsPending] = useValue<boolean>('isPending')
const [results, setResults] = useValue<string[]>('results')

Edit - An open ended in interface implementation

You can define State as an interface, since interfaces are open-ended you can add members when needed. The benefit is if someone else defines a property with the same name but a different type you get an error

interface State {

}
// Don't know what is in here, empty object for starters 
const state : State = {
} as State


const useStore = () => {
    const useValue = <K extends keyof State>(name: K) => {
        const value = state[name]
        const setValue = (value: State[K]): void => {
            state[name] = value
        }
        return [value, setValue] as const
    }

    return { state, useValue }
}
type UseStore = ReturnType<typeof useStore>;

const { useValue } = useStore()

interface State { isPending: boolean }
state.isPending = false; // no guarantee people actually intialize, maybe initial value can be passed to useValue ? 
const [isPending, setIsPending] = useValue('isPending')

interface State { results: string[] }
state.results = ['a', 'b', 'c'];
const [results, setResults] = useValue('results')


interface State { results: number[] } // some else wants to use results for something else, err 
const [results2, setResults2] = useValue('results')
Sign up to request clarification or add additional context in comments.

8 Comments

Thanks a lot for your concise answer. I oversimplified my example without giving enough information about my use case. Basically, the state has a dynamic nature, and could include anything at runtime. What i really want is type information for the last two lines: isPending is a boolean and results is an array of strings. Same applies to the first param of both setters. Therefore i tried to add those types as generics.
@Alp Often anything is not really anything. To paraphrase Ryan Cavanaugh "All programs have types, some developers write them down". You could make useStore itself generic and to account for the different state types
@Alp Added a version that is more inline with what you are trying to achieve. I still like the fully typed version :D. You could have an interface defining the state, and since interfaces are open ended you could add to it from anywhere you needed extra properties, but you would also get the benefit of collision checking.. anyway your call :)
@Alp Added a version with what I meant.
@Alp this is what a full cross file solution would look like. stackblitz.com/edit/typescript-hksdp3?file=index.ts. This approach is called declaration merging and is used in other places (express for example allows adding to Request/Response by a similar approach)
|

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.