Remplace le context CurrentSimulatorData par un hook

Cela permet d'avoir acces au données du simulateur actuel n'importe ou dans l'app
pull/2524/head
Jérémy Rialland 2023-02-23 12:48:35 +01:00 committed by Jérémy Rialland
parent 07f95d75a0
commit 267aab464d
26 changed files with 173 additions and 160 deletions

View File

@ -1,5 +1,6 @@
import { ComponentPropsWithoutRef } from 'react'
import { useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
import Meta from '@/components/utils/Meta'
@ -11,36 +12,19 @@ import { Emoji } from '@/design-system/emoji'
import { Spacing } from '@/design-system/layout'
import { H1 } from '@/design-system/typography/heading'
import { Intro } from '@/design-system/typography/paragraphs'
import { useCurrentSimulatorData } from '@/hooks/useCurrentSimulatorData'
import { situationSelector } from '@/selectors/simulationSelectors'
import { TrackChapter } from '../ATInternetTracking'
import { NextSteps } from '../pages/Simulateurs/NextSteps'
import {
CurrentSimulatorDataProvider,
ExtractFromSimuData,
} from '../pages/Simulateurs/metadata'
import BetaBanner from './BetaBanner'
export interface PageDataProps {
meta: ExtractFromSimuData<'meta'>
simulation?: ExtractFromSimuData<'simulation'>
tracking: ExtractFromSimuData<'tracking'>
tooltip?: ExtractFromSimuData<'tooltip'>
iframePath: ExtractFromSimuData<'iframePath'>
seoExplanations?: ExtractFromSimuData<'seoExplanations'>
beta?: ExtractFromSimuData<'beta'>
nextSteps?: ExtractFromSimuData<'nextSteps'>
path: ExtractFromSimuData<'path'>
title: ExtractFromSimuData<'title'>
private?: ExtractFromSimuData<'private'>
component: ExtractFromSimuData<'component'>
icône: ExtractFromSimuData<'icône'>
pathId: ExtractFromSimuData<'pathId'>
shortName: ExtractFromSimuData<'shortName'>
id: ExtractFromSimuData<'id'>
}
export default function PageData(props: PageDataProps) {
export default function PageData() {
const { currentSimulatorData } = useCurrentSimulatorData()
const { pathname, search } = useLocation()
if (!currentSimulatorData) {
throw new Error(`No simulator found with url: ${pathname}?${search}`)
}
const {
meta,
simulation,
@ -54,7 +38,7 @@ export default function PageData(props: PageDataProps) {
seoExplanations: SeoExplanations,
nextSteps,
path,
} = props
} = currentSimulatorData
const situation = useSelector(situationSelector)
const année =
@ -74,49 +58,47 @@ export default function PageData(props: PageDataProps) {
} as ComponentPropsWithoutRef<typeof TrackChapter>
return (
<CurrentSimulatorDataProvider value={props}>
<TrackChapter {...trackInfo}>
{meta && <Meta page={`simulateur.${title ?? ''}`} {...meta} />}
<TrackChapter {...trackInfo}>
{meta && <Meta page={`simulateur.${title ?? ''}`} {...meta} />}
{beta && <BetaBanner />}
{title && !inIframe && (
<>
<H1>
<StyledSpan>{title}</StyledSpan>
{year && (
<Chip type="secondary" icon={<Emoji emoji="📆" />}>
{year}
</Chip>
)}
{beta && (
<Chip type="info" icon={<Emoji emoji="🚧" />}>
Version bêta
</Chip>
)}
</H1>
{tooltip && <Intro>{tooltip}</Intro>}
</>
)}
<Component />
{!inIframe && (
<>
{SeoExplanations && (
<section>
<SeoExplanations />
</section>
{beta && <BetaBanner />}
{title && !inIframe && (
<>
<H1>
<StyledSpan>{title}</StyledSpan>
{year && (
<Chip type="secondary" icon={<Emoji emoji="📆" />}>
{year}
</Chip>
)}
<NextSteps
iframePath={privateIframe ? undefined : iframePath}
nextSteps={nextSteps}
/>
{beta && (
<Chip type="info" icon={<Emoji emoji="🚧" />}>
Version bêta
</Chip>
)}
</H1>
{tooltip && <Intro>{tooltip}</Intro>}
</>
)}
<Spacing lg />
</>
)}
</TrackChapter>
</CurrentSimulatorDataProvider>
<Component />
{!inIframe && (
<>
{SeoExplanations && (
<section>
<SeoExplanations />
</section>
)}
<NextSteps
iframePath={privateIframe ? undefined : iframePath}
nextSteps={nextSteps}
/>
<Spacing lg />
</>
)}
</TrackChapter>
)
}

View File

@ -7,13 +7,13 @@ import { PopoverWithTrigger } from '@/design-system'
import { Button } from '@/design-system/buttons'
import { Emoji } from '@/design-system/emoji'
import { Grid, Spacing } from '@/design-system/layout'
import { useCurrentSimulatorData } from '@/hooks/useCurrentSimulatorData'
import {
companySituationSelector,
situationSelector,
} from '@/selectors/simulationSelectors'
import { TrackingContext } from '../../ATInternetTracking'
import { CurrentSimulatorDataContext } from '../../pages/Simulateurs/metadata'
import { PlaceDesEntreprisesButton } from '../PlaceDesEntreprises'
import { useParamsFromSituation } from '../utils/useSearchParamsSimulationSharing'
import { ShareSimulationPopup } from './ShareSimulationPopup'
@ -26,7 +26,7 @@ export function useUrl() {
}
const searchParams = useParamsFromSituation(situation)
const currentSimulatorData = useContext(CurrentSimulatorDataContext)
const { currentSimulatorData } = useCurrentSimulatorData()
const { path = '' } = currentSimulatorData ?? {}
const siteUrl =

View File

@ -1,6 +1,6 @@
import { DottedName } from 'modele-social'
import { PublicodesExpression, RuleNode, utils } from 'publicodes'
import { useCallback, useContext, useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
@ -18,7 +18,7 @@ import { Strong } from '@/design-system/typography'
import { H2, H3 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { CurrentSimulatorDataContext } from '@/pages/Simulateurs/metadata'
import { useCurrentSimulatorData } from '@/hooks/useCurrentSimulatorData'
import { isCompanyDottedName } from '@/reducers/companySituationReducer'
import {
answeredQuestionsSelector,
@ -39,7 +39,7 @@ type AnswerListProps = {
export default function AnswerList({ onClose, children }: AnswerListProps) {
const { t } = useTranslation()
const currentSimulatorData = useContext(CurrentSimulatorDataContext)
const { currentSimulatorData } = useCurrentSimulatorData()
const dispatch = useDispatch()
const engine = useEngine()
const situation = useSelector(situationSelector)

View File

@ -1,6 +1,6 @@
import { DottedName } from 'modele-social'
import Engine, { PublicodesExpression } from 'publicodes'
import React, { useContext } from 'react'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
@ -14,7 +14,7 @@ import { Emoji } from '@/design-system/emoji'
import { Grid, Spacing } from '@/design-system/layout'
import { H3 } from '@/design-system/typography/heading'
import { Body } from '@/design-system/typography/paragraphs'
import { CurrentSimulatorDataContext } from '@/pages/Simulateurs/metadata'
import { useCurrentSimulatorData } from '@/hooks/useCurrentSimulatorData'
import {
answeredQuestionsSelector,
situationSelector,
@ -40,7 +40,7 @@ export default function Conversation({
customSituationVisualisation,
engines,
}: ConversationProps) {
const currentSimulatorData = useContext(CurrentSimulatorDataContext)
const { currentSimulatorData } = useCurrentSimulatorData()
const dispatch = useDispatch()
const engine = useEngine()

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import useSimulatorsData from '@/pages/Simulateurs/metadata'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { useSitePaths } from '@/sitePaths'
const PAGE_TITLE = 'Un avis sur cette page ?'

View File

@ -7,8 +7,7 @@ import { SmallCard } from '@/design-system/card'
import { Emoji } from '@/design-system/emoji'
import { Grid } from '@/design-system/layout'
import { H3 } from '@/design-system/typography/heading'
import { ExtractFromSimuData } from '@/pages/Simulateurs/metadata'
import { MetadataSrc } from '@/pages/Simulateurs/metadata-src'
import { MergedSimulatorDataValues } from '@/hooks/useCurrentSimulatorData'
import { useSitePaths } from '@/sitePaths'
import { Highlight } from './Hightlight'
@ -16,7 +15,7 @@ import { Highlight } from './Hightlight'
type AlgoliaSimulatorHit = Hit<{
icône: string
title: string
pathId: MetadataSrc[keyof MetadataSrc]['pathId']
pathId: MergedSimulatorDataValues['pathId']
}>
type SimulatorHitsProps = {
@ -28,8 +27,8 @@ const SimulateurCardHit = ({
path,
tooltip,
}: {
path: ExtractFromSimuData<'path'>
tooltip?: ExtractFromSimuData<'tooltip'>
path: MergedSimulatorDataValues['path']
tooltip?: MergedSimulatorDataValues['tooltip']
hit: AlgoliaSimulatorHit
}) => {
return (
@ -73,7 +72,7 @@ export const SimulatorHits = connectHits<
.reduce<unknown>(
(acc, curr) => (acc as Record<string, unknown>)[curr],
absoluteSitePaths
) as ExtractFromSimuData<'path'>
) as MergedSimulatorDataValues['path']
}
/>
</Grid>

View File

@ -12,7 +12,7 @@ import { Container, Grid, Spacing } from '@/design-system/layout'
import { H2 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { SmallBody } from '@/design-system/typography/paragraphs'
import { CurrentSimulatorDataContext } from '@/pages/Simulateurs/metadata'
import { useCurrentSimulatorData } from '@/hooks/useCurrentSimulatorData'
export default function SalaryExplanation() {
const payslipRef = useRef<HTMLDivElement>(null)
@ -108,7 +108,7 @@ export default function SalaryExplanation() {
function RevenueRepartitionSection(props: { onSeePayslip: () => void }) {
const { t } = useTranslation()
const { colors } = useContext(ThemeContext)
const currentSimulatorData = useContext(CurrentSimulatorDataContext)
const { currentSimulatorData } = useCurrentSimulatorData()
return (
<section>

View File

@ -0,0 +1,24 @@
import { useLocation } from 'react-router-dom'
import { Merge, ToOptional } from '@/types/utils'
import useSimulatorsData, { SimulatorDataValues } from './useSimulatorsData'
export type MergedSimulatorDataValues = ToOptional<Merge<SimulatorDataValues>>
/**
* Gets the current simulator data from url
*/
export const useCurrentSimulatorData = () => {
const simulatorsData = useSimulatorsData()
const pathname = decodeURI(useLocation().pathname)
const [key, data] =
Object.entries(simulatorsData).find(
([, data]) =>
pathname.startsWith(data.path) ||
pathname.startsWith('/iframes/' + data.iframePath)
) ?? []
return { key, currentSimulatorData: data as MergedSimulatorDataValues }
}

View File

@ -0,0 +1,26 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import getMetadataSrc from '@/pages/Simulateurs/metadata-src'
import { useSitePaths } from '@/sitePaths'
export type SimulatorData = ReturnType<typeof getMetadataSrc>
export type SimulatorDataValues = SimulatorData[keyof SimulatorData]
/**
* Gets all simulator data
*/
export default function useSimulatorsData(): SimulatorData {
const [t, i18n] = useTranslation()
const { absoluteSitePaths } = useSitePaths()
return useMemo(
() =>
getMetadataSrc({
t,
sitePaths: absoluteSitePaths,
language: i18n.language,
}),
[t, absoluteSitePaths, i18n.language]
)
}

View File

@ -9,13 +9,13 @@ import { Card } from '@/design-system/card'
import { Grid, Spacing } from '@/design-system/layout'
import { H2 } from '@/design-system/typography/heading'
import { Intro, SmallBody } from '@/design-system/typography/paragraphs'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { RootState } from '@/reducers/rootReducer'
import { useNextQuestionUrl } from '@/selectors/companyStatusSelectors'
import { useSitePaths } from '@/sitePaths'
import { TrackPage } from '../../../ATInternetTracking'
import { SimulateurCard } from '../../Simulateurs/Home'
import useSimulatorsData from '../../Simulateurs/metadata'
import créerSvg from './créer.svg'
export default function Créer() {

View File

@ -4,8 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { Button } from '@/design-system/buttons'
import { H2 } from '@/design-system/typography/heading'
import useSimulatorsData from '../Simulateurs/metadata'
import useSimulatorsData from '@/hooks/useSimulatorsData'
export default function IntegrationTest() {
const simulators = useSimulatorsData()

View File

@ -2,9 +2,9 @@ import { Helmet } from 'react-helmet-async'
import { Route, Routes } from 'react-router-dom'
import Route404 from '@/components/Route404'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import SimulateurPage from '../../components/PageData'
import useSimulatorsData from '../Simulateurs/metadata'
import IframeFooter from './IframeFooter'
export default function Iframes() {
@ -32,7 +32,7 @@ export default function Iframes() {
<Helmet>
<link rel="canonical" href={s.path} />
</Helmet>
<SimulateurPage {...s} />
<SimulateurPage />
</>
}
/>

View File

@ -11,11 +11,11 @@ import { H2 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { useGetFullURL } from '@/hooks/useGetFullURL'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { useSitePaths } from '@/sitePaths'
import { TrackPage } from '../../ATInternetTracking'
import { SimulateurCard } from '../Simulateurs/Home'
import useSimulatorsData from '../Simulateurs/metadata'
import SearchOrCreate from './SearchOrCreate'
import illustration2Svg from './illustration2.svg'
import illustrationSvg from './illustration.svg'

View File

@ -4,11 +4,11 @@ import styled from 'styled-components'
import { H1, H3 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Li, Ul } from '@/design-system/typography/list'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { useSitePaths } from '@/sitePaths'
import { TrackPage } from '../ATInternetTracking'
import Meta from '../components/utils/Meta'
import useSimulatorsData from './Simulateurs/metadata'
export default function Plan() {
const { absoluteSitePaths } = useSitePaths()

View File

@ -13,15 +13,13 @@ import { H2, H3 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Li, Ul } from '@/design-system/typography/list'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { MergedSimulatorDataValues } from '@/hooks/useCurrentSimulatorData'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { useSitePaths } from '@/sitePaths'
import { TrackPage } from '../../ATInternetTracking'
import Meta from '../../components/utils/Meta'
import simulatorSvg from './images/illustration-simulateur.svg'
import useSimulatorsData, {
ExtractFromSimuData,
SimulatorData,
} from './metadata'
export default function Simulateurs() {
const { t } = useTranslation()
@ -210,10 +208,7 @@ export default function Simulateurs() {
)
}
type SimulateurCardProps = SimulatorData[keyof SimulatorData] & {
simulation?: ExtractFromSimuData<'simulation'>
tooltip?: ExtractFromSimuData<'tooltip'>
beta?: ExtractFromSimuData<'beta'>
type SimulateurCardProps = MergedSimulatorDataValues & {
small?: boolean
fromGérer?: boolean
role?: string

View File

@ -4,16 +4,16 @@ import { Condition } from '@/components/EngineValue'
import { useEngine } from '@/components/utils/EngineContext'
import { Grid } from '@/design-system/layout'
import { H2 } from '@/design-system/typography/heading'
import { MergedSimulatorDataValues } from '@/hooks/useCurrentSimulatorData'
import { FAQAutoEntrepreneurArticle } from '@/pages/Creer/CreationChecklist'
import { GuideURSSAFCard } from '@/pages/Simulateurs/cards/GuideURSSAFCard'
import { IframeIntegrationCard } from '@/pages/Simulateurs/cards/IframeIntegrationCard'
import { SimulatorRessourceCard } from '@/pages/Simulateurs/cards/SimulatorRessourceCard'
import { ExtractFromSimuData } from '@/pages/Simulateurs/metadata'
import { useSitePaths } from '@/sitePaths'
interface NextStepsProps {
iframePath?: ExtractFromSimuData<'iframePath'>
nextSteps: ExtractFromSimuData<'nextSteps'>
iframePath?: MergedSimulatorDataValues['iframePath']
nextSteps: MergedSimulatorDataValues['nextSteps']
}
export function NextSteps({ iframePath, nextSteps }: NextStepsProps) {

View File

@ -2,8 +2,7 @@ import { Trans } from 'react-i18next'
import { Article } from '@/design-system/card'
import { Emoji } from '@/design-system/emoji'
import useSimulatorsData, { SimulatorData } from '../metadata'
import useSimulatorsData, { SimulatorData } from '@/hooks/useSimulatorsData'
type SimulatorRessourceCardProps = {
simulatorId: keyof SimulatorData

View File

@ -6,11 +6,11 @@ import { ScrollToTop } from '@/components/utils/Scroll'
import { usePersistingState } from '@/components/utils/persistState'
import { useIsEmbedded } from '@/components/utils/useIsEmbedded'
import { Link } from '@/design-system/typography/link'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { useSitePaths } from '@/sitePaths'
import SimulateurPage from '../../components/PageData'
import Home from './Home'
import useSimulatorsData from './metadata'
type State = {
fromGérer?: boolean
@ -43,7 +43,7 @@ export default function Simulateurs() {
path={
s.path.replace(absoluteSitePaths.simulateurs.index, '') + '/*'
}
element={<SimulateurPage {...s} />}
element={<SimulateurPage />}
/>
)),
[simulatorsData, absoluteSitePaths]

View File

@ -1,46 +0,0 @@
import { createContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useSitePaths } from '@/sitePaths'
import getMetadataSrc from './metadata-src'
export type SimulatorData = ReturnType<typeof getMetadataSrc>
/**
* Extract type if U extends T
* Else return an object with value undefined
*/
type ExtractOrUndefined<T, U> = T extends U ? T : Record<keyof U, undefined>
/**
* Extract type from a key of SimulatorData
*/
export type ExtractFromSimuData<T extends string> = ExtractOrUndefined<
SimulatorData[keyof SimulatorData],
Record<T, unknown>
>[T]
export default function useSimulatorsData(): SimulatorData {
const [t, i18n] = useTranslation()
const { absoluteSitePaths } = useSitePaths()
return useMemo(
() =>
getMetadataSrc({
t,
sitePaths: absoluteSitePaths,
language: i18n.language,
}),
[t, absoluteSitePaths, i18n.language]
)
}
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U
export const CurrentSimulatorDataContext = createContext<Overwrite<
SimulatorData[keyof SimulatorData],
{ path: ExtractFromSimuData<'path'> }
> | null>(null)
export const CurrentSimulatorDataProvider = CurrentSimulatorDataContext.Provider

View File

@ -1,8 +1,8 @@
import { H2 } from '@/design-system/typography/heading'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { TrackPage } from '../../../ATInternetTracking'
import { SimulateurCard } from '../Home'
import useSimulatorsData from '../metadata'
export default function SalariéSimulation() {
const simulators = useSimulatorsData()

View File

@ -5,9 +5,9 @@ import styled from 'styled-components'
import { TrackChapter } from '@/ATInternetTracking'
import { useIsEmbedded } from '@/components/utils/useIsEmbedded'
import { Link } from '@/design-system/typography/link'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { useSitePaths } from '@/sitePaths'
import useSimulatorsData from '../metadata'
import Activité from './Activité'
import ActivitésSelection from './ActivitésSelection'
import { StoreProvider } from './StoreContext'

View File

@ -16,11 +16,11 @@ import { Grid, Spacing } from '@/design-system/layout'
import { H2, H3 } from '@/design-system/typography/heading'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { useFetchData } from '@/hooks/useFetchData'
import useSimulatorsData, { SimulatorData } from '@/hooks/useSimulatorsData'
import { toAtString } from '../../ATInternetTracking'
import { debounce, groupBy } from '../../utils'
import { SimulateurCard } from '../Simulateurs/Home'
import useSimulatorsData, { SimulatorData } from '../Simulateurs/metadata'
import Chart, { Data, isDataStacked } from './Chart'
import DemandeUtilisateurs from './DemandesUtilisateurs'
import GlobalStats, { BigIndicator } from './GlobalStats'

View File

@ -1,10 +1,10 @@
import { Route, Routes } from 'react-router-dom'
import Route404 from '@/components/Route404'
import useSimulatorsData from '@/hooks/useSimulatorsData'
import { useSitePaths } from '@/sitePaths'
import SimulateurPage from '../../components/PageData'
import useSimulatorsData from '../Simulateurs/metadata'
export default function Assistants() {
const sitePaths = useSitePaths()
@ -19,7 +19,7 @@ export default function Assistants() {
sitePaths.absoluteSitePaths.assistants.index,
''
)}
element={<SimulateurPage {...simu} />}
element={<SimulateurPage />}
/>
))

View File

@ -46,13 +46,13 @@ import { Link } from '@/design-system/typography/link'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import { useQuestionList } from '@/hooks/useQuestionList'
import { useSetEntreprise } from '@/hooks/useSetEntreprise'
import useSimulatorsData, { SimulatorData } from '@/hooks/useSimulatorsData'
import { companySituationSelector } from '@/selectors/simulationSelectors'
import { useSitePaths } from '@/sitePaths'
import { evaluateQuestion } from '@/utils'
import { TrackChapter, TrackPage } from '../../ATInternetTracking'
import { SimulateurCard } from '../Simulateurs/Home'
import useSimulatorsData, { SimulatorData } from '../Simulateurs/metadata'
import { AnnuaireEntreprises } from './components/AnnuaireEntreprises'
import { AutoEntrepreneurCard } from './components/AutoEntrepeneurCard'
import { DemarcheEmbaucheCard } from './components/DemarcheEmbauche'
@ -112,7 +112,7 @@ export default function Gérer() {
element={
<>
{back}
<PageData {...p} />
<PageData />
</>
}
/>

View File

@ -13,11 +13,11 @@ import { Grid, Spacing } from '@/design-system/layout'
import { H1, H2, H3 } from '@/design-system/typography/heading'
import { Link } from '@/design-system/typography/link'
import { Body, Intro } from '@/design-system/typography/paragraphs'
import useSimulatorsData, { SimulatorData } from '@/hooks/useSimulatorsData'
import urssafLogo from '@/images/Urssaf.svg'
import { TrackPage } from '../../ATInternetTracking'
import Meta from '../../components/utils/Meta'
import useSimulatorsData, { SimulatorData } from '../Simulateurs/metadata'
import './iframe.css'

View File

@ -1,5 +1,8 @@
// Immutable type
// source: https://github.com/cefn/lauf/blob/b982a09/modules/store/src/types/immutable.ts#L25
/**
* Immutable type
*
* source: https://github.com/cefn/lauf/blob/b982a09/modules/store/src/types/immutable.ts#L25
*/
export type ImmutableType<T> = T extends (...args: unknown[]) => unknown
? T
: T extends object
@ -9,3 +12,35 @@ export type ImmutableType<T> = T extends (...args: unknown[]) => unknown
type ImmutableIndex<T> = Readonly<{
[K in keyof T]: ImmutableType<T[K]>
}>
/**
* Merge union of object
*
* Example: `Merge<{a: number, b: string} | {a: string, c: string}>` type gives
* `{a: string | number; b: string | undefined; c: string | undefined;}`
*/
export type Merge<T extends object> = {
[k in AllKeys<T>]: PickTypeOf<T, k>
}
type AllKeys<T> = T extends unknown ? keyof T : never
type PickType<T, K extends AllKeys<T>> = T extends { [k in K]: unknown }
? T[K]
: undefined
type PickTypeOf<T, K extends string | number | symbol> = K extends AllKeys<T>
? PickType<T, K>
: never
/**
* Transform undefined value of an object to optional value
*
* Example: `ToOptional<{a: number, b: string | undefined}>` type gives `{a: number; b?: string;}`
*/
export type ToOptional<T> = Partial<Pick<T, UndefinedProperties<T>>> &
Pick<T, Exclude<keyof T, UndefinedProperties<T>>>
type UndefinedProperties<T> = {
[P in keyof T]-?: undefined extends T[P] ? P : never
}[keyof T]