【问题标题】:Typescript Generics wrapper: Untyped function calls may not accept type argumentsTypescript 泛型包装器:无类型函数调用可能不接受类型参数
【发布时间】:2019-04-22 07:27:50
【问题描述】:

这个带有泛型的代码 sn-p 工作得很好 (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')

在这里我可以确定isPending 是一个布尔值,setIsPending 接收一个布尔值作为参数。同样适用于 resultssetResults 作为字符串数组。

然后我用另一种方法包装代码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')

最后两行给我打字错误:Untyped function calls may not accept type arguments.

我怀疑useStore 接口有问题,但由于它的动态特性,我想不出更好的解决方案。

如何在使用 Generic 类型获得正确的类型提示和代码完成时摆脱错误?

【问题讨论】:

    标签: javascript typescript generics


    【解决方案1】:

    由于useValue 的类型是Function,因此传入泛型类型参数是没有意义的。他们对谁有利?运行时没有得到它们,它们在编译器时被擦除。编译器不能使用它们,因为Function 只是一个无类型函数,因此没有任何好处。传递类型参数是无用的,并且可以说是一个错误(即用户没想到这是Function,并且正在传递类型参数,认为它们有一些效果)。

    删除类型参数并放弃以任何方式输入的假装:

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

    更有趣的问题是你为什么要编写这样的代码,因为有一种方法可以完全键入这段代码中的所有内容:

    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')
    

    上面的版本是完全类型安全的,不需要任何名称或类型的重复(您当然可以将其拆分为多个文件,但根据您的要求可能需要发生一些重复)。如果这不适用于您的情况,我很想知道原因。

    编辑

    如果您只是希望类型在最后几行得到解决并在那里具有某种类型安全性,您只需要使用泛型为函数指定签名:

    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')
    

    编辑 - 开放式接口实现

    您可以将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')
    

    【讨论】:

    • 非常感谢您简洁的回答。我过度简化了我的示例,但没有提供有关我的用例的足够信息。基本上,state 具有动态特性,可以在运行时包含任何内容。我真正想要的是最后两行的类型信息:isPending 是一个布尔值,results 是一个字符串数组。这同样适用于两个 setter 的第一个参数。因此我尝试将这些类型添加为泛型。
    • @Alp 通常什么都不是什么。套用 Ryan Cavanaugh “所有程序都有类型,一些开发人员将它们写下来”。您可以将 useStore 本身设为通用并考虑不同的状态类型
    • @Alp 添加了一个更符合您要实现的目标的版本。我仍然喜欢完全输入的版本:D。你可以有一个定义状态的接口,并且由于接口是开放式的,你可以从任何你需要额外属性的地方添加到它,但你也可以获得碰撞检查的好处..无论如何你的电话:)
    • @Alp 添加了我的意思的版本。
    • @Alp 这就是完整的交叉文件解决方案的样子。 stackblitz.com/edit/typescript-hksdp3?file=index.ts。这种方法称为声明合并,并在其他地方使用(例如express允许通过类似方法添加到Request/Response
    猜你喜欢
    • 2018-12-30
    • 2018-03-02
    • 2018-06-22
    • 2022-01-12
    • 2021-10-06
    • 2018-09-15
    • 2016-11-01
    • 2020-07-10
    • 2020-11-10
    相关资源
    最近更新 更多