diff --git a/apps/vdn-static/src/i18n/index.ts b/apps/vdn-static/src/i18n/index.ts index f262e5b..ef4c791 100644 --- a/apps/vdn-static/src/i18n/index.ts +++ b/apps/vdn-static/src/i18n/index.ts @@ -30,7 +30,12 @@ export interface UseLocaleOptions { } function isObject(value: unknown) { - return value !== null && typeof value === "object"; + return typeof value === "object" && value !== null; +} + +function deepReadonly(value: T): DeepReadonly { + // SAFETY: we're just making an immutable view to the type, this isn't dangerous + return value as DeepReadonly; } function fallbackProxy( @@ -40,8 +45,9 @@ function fallbackProxy( const proxy = new Proxy(fallback, { get: (_target, key): DeepReadonly => { // SAFETY: typescript should ensure we're only ever trying to access keys - // that exist on T, and if the key doesn't, we'll be returning undefined anyway - // which is the expected behavior. + // that exist on T, and if the key doesn't, + // just process its fallback as if it did, + // everything should work as expected still const tKey = key as keyof T; // value may not exist on mask @@ -54,26 +60,29 @@ function fallbackProxy( // it may still have missing properties. // this only handles the case where the current value is undefined, not nested ones. // thus, `finalValue` is still a `DeepPartial` (but not undefined) - const finalValue: DeepPartial = + const finalValue: DeepPartial[keyof T] = value === undefined ? fallbackValue : value; - // check if finalValue or fallbackValue is not an object - // (we can't deep proxy unless both of them are objects) - if (!isObject(finalValue) || !isObject(fallbackValue)) { - // SAFETY: finalValue is not an object, - // so `DeepPartial` == `typeof finalValue` - // (it is a primitive type) - // then we apply DeepReadonly to disallow mutations and to satisfy - // the return type of this getter function - return finalValue as DeepReadonly; + // check if finalValue is not an object + // if not, it is a primitive + if (!isObject(finalValue)) { + // SAFETY: finalValue is not an object, so it is not affected by DeepPartial + // so `DeepPartial[keyof T]` is the same as `T[keyof T]` + return deepReadonly(finalValue as T[keyof T]); + } + + // else, finalValue is an object, so we need to proxy it as well + + // check if fallbackValue is an object so that it can be used as finalValue's fallback + if (!isObject(fallbackValue)) { + // if not, we can't use finalValue as we'll have no fallback for it. + // send the fallbackValue no matter what instead + return deepReadonly(fallbackValue); } // else, proxy the returned object to support deep fallback proxying return fallbackProxy( - // SAFETY: we validate that finalValue is an object above. - // I don't know why TypeScript isn't narrowing the type for us - // based on that predicate, but oh well - finalValue as DeepPartial & object, + finalValue, fallbackValue, ); }, @@ -82,7 +91,6 @@ function fallbackProxy( }, }); - // SAFETY: we're just disallowing mutations to the proxy, - // since its setter panics if used at runtime - return proxy as DeepReadonly; + // we're just disallowing mutations to the proxy, since its setter panics if used at runtime + return deepReadonly(proxy); } diff --git a/apps/vdn-static/src/utils/deep-partial.ts b/apps/vdn-static/src/utils/deep-partial.ts index ef71848..7e0f0b3 100644 --- a/apps/vdn-static/src/utils/deep-partial.ts +++ b/apps/vdn-static/src/utils/deep-partial.ts @@ -1 +1,3 @@ -export type DeepPartial = { [K in keyof T]?: DeepPartial }; +export type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; +};