refactor: stricter types

This commit is contained in:
EnixCoda 2019-02-20 12:16:27 +08:00
parent b01a4c418c
commit f39ba9c96f
No known key found for this signature in database
GPG key ID: 6825847C88AA329A
5 changed files with 115 additions and 56 deletions

View file

@ -1,16 +1,9 @@
import * as React from 'react'
export type ParametersOfReturnedFunction<Func> = Func extends (<Args extends []>(
...args1: any[] // it's ok
) => (...args2: Args) => any) // it's ok
? Args
: never
export type Method = (...args: Args) => void | Promise<void>
type Args = any[] // it's ok
export type Method<Args = any[]> = (...args: Args extends any[] ? Args : []) => void | Promise<void>
export type Middleware = <M extends Method, MM extends Method>(
method: M,
args: Parameters<M>
args: Parameters<M>,
) => [MM | M, Parameters<MM | M>]
const middlewares: Middleware[] = []
@ -40,11 +33,11 @@ function run<M extends Method>([method, args]: [M, Parameters<M>]) {
export type DispatchState<Props, State> = React.Component<Props, State>['setState']
export type PreDispatch<Props, State> = (
dispatchCallback: (state: State, props: Props) => Props | Promise<void> | void,
callback?: () => void
callback?: () => void,
) => void
export type TriggerOtherMethod = <MC extends MethodCreator<any, any>>(
export type TriggerOtherMethod = <MC extends MethodCreator<any, any, any[]>>(
methodCreator: MC,
...args: ParametersOfReturnedFunction<MC>
...args: Parameters<ReturnType<MC>>
) => void
export type Dispatch<Props, State> = {
@ -53,20 +46,25 @@ export type Dispatch<Props, State> = {
call: TriggerOtherMethod
}
export type MethodCreator<Props, State> = (dispatch: Dispatch<Props, State>) => Method
export type MethodCreator<Props, State, Args = []> = (
dispatch: Dispatch<Props, State>,
) => Method<Args>
type Sources = {
[key: string]: MethodCreator<any, any>
type Sources<P, S> = {
[key: string]: MethodCreator<P, S, any[]>
}
type WrappedMethods = {
[key: string]: Method
}
function link<P, S>(instance: React.Component<P, S>, sources: Sources): WrappedMethods {
function link<P, S>(instance: React.Component<P, S>, sources: Sources<P, S>): WrappedMethods {
const wrappedMethods: WrappedMethods = {
/* [keyof sources] -> wrappedMethods.method */
}
const map = new Map<MethodCreator<P, S>, Method>(/* sources.creator -> wrappedMethods.method */)
const map = new Map<
MethodCreator<P, S, any[]>,
Method
>(/* sources.creator -> wrappedMethods.method */)
const dispatchCall: TriggerOtherMethod = (createMethod, ...otherArgs) => {
const isFromSource = sourcesValues.includes(createMethod)
@ -99,9 +97,9 @@ function link<P, S>(instance: React.Component<P, S>, sources: Sources): WrappedM
return wrappedMethods
}
export default function connect<BaseP, ExtraP>(mapping: Sources) {
export default function connect<BaseP, ExtraP>(mapping: Sources<BaseP, ExtraP>) {
return function linkComponent<S>(
ComponentClass: React.ComponentClass<BaseP & ExtraP, S>
ComponentClass: React.ComponentClass<BaseP & ExtraP, S>,
): React.ComponentClass<BaseP, ExtraP> {
return class AwesomeApp extends React.PureComponent<BaseP, ExtraP> {
static displayName = `Connected(${ComponentClass.displayName || ComponentClass.name})`

View file

@ -39,7 +39,8 @@ type Task = () => void
const tasksAfterRender: (Task)[] = []
const visibleNodesGenerator = new VisibleNodesGenerator()
const init: MethodCreator<Props, ConnectorState> = dispatch => () => dispatch.call(setStateText, 'Fetching File List...')
const init: MethodCreator<Props, ConnectorState> = dispatch => () =>
dispatch.call(setStateText, 'Fetching File List...')
function resolveGitModules(root: TreeNode, blobData: BlobData) {
if (blobData) {
@ -79,7 +80,7 @@ const setUpTree: MethodCreator<Props, ConnectorState> = dispatch => () =>
await visibleNodesGenerator.plantTree(root as TreeNode)
tasksAfterRender.push(DOMHelper.focusSearchInput)
dispatch.call(setStateText, null)
dispatch.call(setStateText, '')
const currentPath = URLHelper.getCurrentPath(metaData.branchName)
if (currentPath.length) {
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
@ -99,12 +100,20 @@ const execAfterRender: MethodCreator<Props, ConnectorState> = dispatch => () =>
tasksAfterRender.length = 0
}
const setStateText: MethodCreator<Props, ConnectorState> = dispatch => (text: string) =>
const setStateText: MethodCreator<
Props,
ConnectorState,
[ConnectorState['stateText']]
> = dispatch => (text: string) =>
dispatch.set({
stateText: text,
})
const handleKeyDown: MethodCreator<Props, ConnectorState> = dispatch => event =>
const handleKeyDown: MethodCreator<
Props,
ConnectorState,
[React.KeyboardEvent]
> = dispatch => event =>
dispatch.get(({ visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
function handleVerticalMove(index: number) {
if (0 <= index && index < nodes.length) {
@ -201,12 +210,17 @@ const handleKeyDown: MethodCreator<Props, ConnectorState> = dispatch => event =>
}
})
const onFocusSearchBar: MethodCreator<Props, ConnectorState> = dispatch => () => dispatch.call(focusNode, null)
const onFocusSearchBar: MethodCreator<Props, ConnectorState> = dispatch => () =>
dispatch.call(focusNode, null)
const handleSearchKeyChange: MethodCreator<Props, ConnectorState> = dispatch => {
const handleSearchKeyChange: MethodCreator<
Props,
ConnectorState,
[React.FormEvent<HTMLInputElement>]
> = dispatch => {
let i = 0
return async event => {
const searchKey = event.target.value
const searchKey = event.currentTarget.value
const j = (i += 1)
await visibleNodesGenerator.search(searchKey)
if (i === j) dispatch.call(updateVisibleNodes)
@ -221,7 +235,10 @@ function shouldDelayExpand(node: TreeNode) {
)
}
const setExpand: MethodCreator<Props, ConnectorState> = dispatch => (node, expand) => {
const setExpand: MethodCreator<Props, ConnectorState, [TreeNode, boolean]> = dispatch => (
node,
expand = false
) => {
visibleNodesGenerator.setExpand(node, expand)
const applyChanges = () => dispatch.call(focusNode, node)
if (shouldDelayExpand(node)) {
@ -232,7 +249,10 @@ const setExpand: MethodCreator<Props, ConnectorState> = dispatch => (node, expan
}
}
const toggleNodeExpansion: MethodCreator<Props, ConnectorState> = dispatch => (node, skipScrollToNode) => {
const toggleNodeExpansion: MethodCreator<Props, ConnectorState, [TreeNode, boolean]> = dispatch => (
node,
skipScrollToNode
) => {
visibleNodesGenerator.toggleExpand(node)
const applyChanges = () => {
dispatch.call(focusNode, node, skipScrollToNode)
@ -246,7 +266,11 @@ const toggleNodeExpansion: MethodCreator<Props, ConnectorState> = dispatch => (n
}
}
const focusNode: MethodCreator<Props, ConnectorState> = dispatch => (node, skipScroll) =>
const focusNode: MethodCreator<
Props,
ConnectorState,
[TreeNode] | [TreeNode, boolean]
> = dispatch => (node: TreeNode, skipScroll = false) =>
dispatch.get(({ visibleNodes: { nodes } }) => {
visibleNodesGenerator.focusNode(node)
if (node && !skipScroll) {
@ -257,7 +281,7 @@ const focusNode: MethodCreator<Props, ConnectorState> = dispatch => (node, skipS
dispatch.call(updateVisibleNodes)
})
const onNodeClick: MethodCreator<Props, ConnectorState> = dispatch => node => {
const onNodeClick: MethodCreator<Props, ConnectorState, [TreeNode]> = dispatch => node => {
if (node.type === 'tree') {
dispatch.call(toggleNodeExpansion, node, true)
} else if (node.type === 'blob') {
@ -268,7 +292,11 @@ const onNodeClick: MethodCreator<Props, ConnectorState> = dispatch => node => {
}
}
const mountExpandingIndicator: MethodCreator<Props, ConnectorState> = dispatch => node =>
const mountExpandingIndicator: MethodCreator<
Props,
ConnectorState,
[TreeNode]
> = dispatch => node =>
dispatch.get(({ visibleNodes }) => {
const dummyVisibleNodes = {
...visibleNodes,

View file

@ -44,7 +44,7 @@ export type ConnectorState = {
toggleShowSideBar: () => void
toggleShowSettings: () => void
onAccessTokenChange: SettingsBar['props']['onAccessTokenChange']
onKeyDown: EventListener
onKeyDown: (e: KeyboardEvent) => string
onShortcutChange: SettingsBar['props']['onShortcutChange']
setCopyFile: SettingsBar['props']['setCopyFile']
setCopySnippet: SettingsBar['props']['setCopySnippet']
@ -134,7 +134,7 @@ const init: MethodCreator<Props, ConnectorState> = dispatch => async () => {
}
}
const handleError: MethodCreator<Props, ConnectorState> = dispatch => async err => {
const handleError: MethodCreator<Props, ConnectorState, [Error]> = dispatch => async err => {
if (err.message === EMPTY_PROJECT) {
dispatch.call(setError, 'This project seems to be empty.')
} else if (
@ -160,7 +160,7 @@ const onPJAXEnd: MethodCreator<Props, ConnectorState> = dispatch => () => {
})
}
const onKeyDown: MethodCreator<Props, ConnectorState> = dispatch => e => {
const onKeyDown: MethodCreator<Props, ConnectorState, [KeyboardEvent]> = dispatch => e => {
dispatch.get(({ toggleShowSideBarShortcut }) => {
if (toggleShowSideBarShortcut) {
const keys = keyHelper.parseEvent(e)
@ -172,16 +172,22 @@ const onKeyDown: MethodCreator<Props, ConnectorState> = dispatch => e => {
}
const toggleShowSideBar: MethodCreator<Props, ConnectorState> = dispatch => () =>
dispatch.get(({ shouldShow }) =>
dispatch.call(setShouldShow, !shouldShow)
)
dispatch.get(({ shouldShow }) => dispatch.call(setShouldShow, !shouldShow))
const setShouldShow: MethodCreator<Props, ConnectorState> = dispatch => shouldShow => {
const setShouldShow: MethodCreator<
Props,
ConnectorState,
[ConnectorState['shouldShow']]
> = dispatch => shouldShow => {
dispatch.set({ shouldShow }, shouldShow ? DOMHelper.focusFileExplorer : null)
DOMHelper.setBodyIndent(shouldShow)
}
const setError: MethodCreator<Props, ConnectorState> = dispatch => error => {
const setError: MethodCreator<
Props,
ConnectorState,
[ConnectorState['error']]
> = dispatch => error => {
dispatch.set({ error })
dispatch.call(setShouldShow, false)
}
@ -191,22 +197,47 @@ const toggleShowSettings: MethodCreator<Props, ConnectorState> = dispatch => ()
showSettings: !showSettings,
}))
const setShowSettings: MethodCreator<Props, ConnectorState> = dispatch => showSettings => dispatch.set({ showSettings })
const setShowSettings: MethodCreator<
Props,
ConnectorState,
[ConnectorState['showSettings']]
> = dispatch => showSettings => dispatch.set({ showSettings })
const onAccessTokenChange: MethodCreator<Props, ConnectorState> = dispatch => accessToken => dispatch.set({ accessToken })
const onAccessTokenChange: MethodCreator<
Props,
ConnectorState,
[ConnectorState['accessToken']]
> = dispatch => accessToken => dispatch.set({ accessToken })
const onShortcutChange: MethodCreator<Props, ConnectorState> = dispatch => shortcut =>
dispatch.set({ toggleShowSideBarShortcut: shortcut })
const onShortcutChange: MethodCreator<
Props,
ConnectorState,
[ConnectorState['toggleShowSideBarShortcut']]
> = dispatch => shortcut => dispatch.set({ toggleShowSideBarShortcut: shortcut })
const setMetaData: MethodCreator<Props, ConnectorState> = dispatch => metaData => dispatch.set({ metaData })
const setMetaData: MethodCreator<
Props,
ConnectorState,
[ConnectorState['metaData']]
> = dispatch => metaData => dispatch.set({ metaData })
const setCompressSingleton: MethodCreator<Props, ConnectorState> = dispatch => compressSingletonFolder =>
dispatch.set({ compressSingletonFolder })
const setCompressSingleton: MethodCreator<
Props,
ConnectorState,
[ConnectorState['compressSingletonFolder']]
> = dispatch => compressSingletonFolder => dispatch.set({ compressSingletonFolder })
const setCopyFile: MethodCreator<Props, ConnectorState> = dispatch => copyFileButton => dispatch.set({ copyFileButton })
const setCopyFile: MethodCreator<
Props,
ConnectorState,
[ConnectorState['copyFileButton']]
> = dispatch => copyFileButton => dispatch.set({ copyFileButton })
const setCopySnippet: MethodCreator<Props, ConnectorState> = dispatch => copySnippetButton =>
dispatch.set({ copySnippetButton })
const setCopySnippet: MethodCreator<
Props,
ConnectorState,
[ConnectorState['copySnippetButton']]
> = dispatch => copySnippetButton => dispatch.set({ copySnippetButton })
export default {
init,

View file

@ -28,7 +28,11 @@ function setBodyIndent(shouldShowGitako: boolean) {
}
}
function $(selector: string, existCallback?: (element: Element) => any, otherwise?: () => any) {
function $<R1 = never, R2 = never>(
selector: string,
existCallback?: (element: Element) => R1,
otherwise?: () => R2,
): R1 | R2 | Element {
const element = document.querySelector(selector)
if (element) {
return existCallback ? existCallback(element) : element
@ -52,14 +56,14 @@ function getCurrentBranch() {
'.repository-content > .file-navigation > .branch-select-menu > button'
const branchNameFromButtonElement = $(
selectedBranchButtonSelector,
(element: HTMLButtonElement) => element.title.trim()
(element: HTMLButtonElement) => element.title.trim(),
)
if (branchNameFromButtonElement) return branchNameFromButtonElement
const selectedBranchSelector =
'.select-menu.branch-select-menu .select-menu-modal .select-menu-list .select-menu-item.selected svg.select-menu-item-icon + span'
const branchNameFromSelectElement = $(selectedBranchSelector, element =>
element.textContent.trim()
element.textContent.trim(),
)
if (branchNameFromSelectElement) return branchNameFromSelectElement
}
@ -299,7 +303,7 @@ function attachCopySnippet() {
target.parentNode.insertBefore(clippy, target)
}
}
})
}),
)
}

View file

@ -1,5 +1,3 @@
import { KeyboardEvent } from 'react'
const keyCodeArray = [
...'1234567890abcdefghijklmnopqrstuvwxyz'.split(''),
..."`[]\\;',./".split(''),
@ -38,7 +36,7 @@ function parseKeyCode(code: string) {
return code.toLowerCase().replace(/^control$/, 'ctrl')
}
function parseEvent(e: KeyboardEvent<HTMLInputElement>) {
function parseEvent(e: KeyboardEvent | React.KeyboardEvent) {
const { altKey: alt, shiftKey: shift, metaKey: meta, ctrlKey: ctrl } = e
const code = parseKeyCode(e.key)
const keys = { meta, ctrl, shift, alt, [code]: true }