I was messing with typescript recently and in my application I wanted to enforce deprecated settings sanitizing with the power of static type checking. Basically try and make some pattern that is extensible that if a field was to be deprecated it would force the programmer to sanitize that via removing it or migrating it into a new name, etc. I feel like I am close, but I might not know the exact in and out of guarding to get this to work how I want it.
Basically if the there is a field present in UnsanitizedGlobalSettings that should not be it is handled correctly and the type check system throws an issue.
<code>export interface GlobalSettings {
export type DEPRECATED_GameNotificationSettings = {
/** @deprecated game has been updated to gameV2, please use that */
export type DEPRECATED_CarNotificationSettings = {
/** @deprecated car notifications have been removed */
export type UnsanitizedGlobalSettings =
DEPRECATED_GameNotificationSettings |
DEPRECATED_CarNotificationSettings;
* This function sanitizes and migrates the settings object to the latest version
* @param {UnsanitizedGlobalSettings} settings - The settings to sanitize and migrate
* @return {GlobalSettings} - The settings are mutated and now comply with GlobalSettings
function sanitizeAndMigrateSettings(settings: UnsanitizedGlobalSettings): GlobalSettings {
// Migrate deprecated `game` settings to `gameV2`
if ('game' in settings.notifications && settings.notifications.game !== undefined) {
settings.notifications.gameV2 = {
enabled: settings.notifications.game.enabled
delete settings.notifications.game; // Remove the deprecated `game`
// THIS SHOULD THROW A TYPE ERROR SINCE `car` IS NOT IN GlobalSettings
// Remove deprecated `car` settings
// if ('car' in settings.notifications) {
// delete settings.notifications.car;
const DEFAULT_SETTINGS: GlobalSettings = {
system: { enabled: false },
social: { enabled: false },
gameV2: { enabled: false }
const unsanitizedSettings: UnsanitizedGlobalSettings = {
notifications: { // Hypothetical user settings loaded from a server or something
game: { enabled: true }, // The deprecated `game` setting is in the server still
car: { enabled: true }, // The deprecated `car` setting is in the server still
system: { enabled: true },
social: { enabled: false }
// Sanitize and migrate the settings, mutating the original object
const settings = sanitizeAndMigrateSettings(unsanitizedSettings);
// Now, `unsanitizedSettings` is guaranteed to be of type `GlobalSettings`
console.log(settings); // TypeScript should knows this is a `GlobalSettings` now...
* system: { enabled: true },
* social: { enabled: false },
* gameV2: { enabled: true }
<code>export interface GlobalSettings {
notifications: {
system: {
enabled: boolean;
},
social: {
enabled: boolean;
},
gameV2: {
enabled: boolean;
}
}
}
export type DEPRECATED_GameNotificationSettings = {
notifications: {
/** @deprecated game has been updated to gameV2, please use that */
game?: {
enabled: boolean;
}
}
}
export type DEPRECATED_CarNotificationSettings = {
notifications: {
/** @deprecated car notifications have been removed */
car?: {
enabled: boolean;
}
}
}
export type UnsanitizedGlobalSettings =
GlobalSettings |
DEPRECATED_GameNotificationSettings |
DEPRECATED_CarNotificationSettings;
/**
* This function sanitizes and migrates the settings object to the latest version
*
* @param {UnsanitizedGlobalSettings} settings - The settings to sanitize and migrate
* @return {GlobalSettings} - The settings are mutated and now comply with GlobalSettings
*/
function sanitizeAndMigrateSettings(settings: UnsanitizedGlobalSettings): GlobalSettings {
// Migrate deprecated `game` settings to `gameV2`
if ('game' in settings.notifications && settings.notifications.game !== undefined) {
settings.notifications.gameV2 = {
enabled: settings.notifications.game.enabled
};
delete settings.notifications.game; // Remove the deprecated `game`
}
// THIS SHOULD THROW A TYPE ERROR SINCE `car` IS NOT IN GlobalSettings
// AND SHOULD BE REMOVED
// Remove deprecated `car` settings
// if ('car' in settings.notifications) {
// delete settings.notifications.car;
// }
return settings;
}
const DEFAULT_SETTINGS: GlobalSettings = {
notifications: {
system: { enabled: false },
social: { enabled: false },
gameV2: { enabled: false }
}
}
// Usage:
const unsanitizedSettings: UnsanitizedGlobalSettings = {
...DEFAULT_SETTINGS,
notifications: { // Hypothetical user settings loaded from a server or something
game: { enabled: true }, // The deprecated `game` setting is in the server still
car: { enabled: true }, // The deprecated `car` setting is in the server still
system: { enabled: true },
social: { enabled: false }
}
};
// Sanitize and migrate the settings, mutating the original object
const settings = sanitizeAndMigrateSettings(unsanitizedSettings);
// Now, `unsanitizedSettings` is guaranteed to be of type `GlobalSettings`
console.log(settings); // TypeScript should knows this is a `GlobalSettings` now...
/**
* Expected output:
* {
* notifications: {
* system: { enabled: true },
* social: { enabled: false },
* gameV2: { enabled: true }
* }
* }
*/
</code>
export interface GlobalSettings {
notifications: {
system: {
enabled: boolean;
},
social: {
enabled: boolean;
},
gameV2: {
enabled: boolean;
}
}
}
export type DEPRECATED_GameNotificationSettings = {
notifications: {
/** @deprecated game has been updated to gameV2, please use that */
game?: {
enabled: boolean;
}
}
}
export type DEPRECATED_CarNotificationSettings = {
notifications: {
/** @deprecated car notifications have been removed */
car?: {
enabled: boolean;
}
}
}
export type UnsanitizedGlobalSettings =
GlobalSettings |
DEPRECATED_GameNotificationSettings |
DEPRECATED_CarNotificationSettings;
/**
* This function sanitizes and migrates the settings object to the latest version
*
* @param {UnsanitizedGlobalSettings} settings - The settings to sanitize and migrate
* @return {GlobalSettings} - The settings are mutated and now comply with GlobalSettings
*/
function sanitizeAndMigrateSettings(settings: UnsanitizedGlobalSettings): GlobalSettings {
// Migrate deprecated `game` settings to `gameV2`
if ('game' in settings.notifications && settings.notifications.game !== undefined) {
settings.notifications.gameV2 = {
enabled: settings.notifications.game.enabled
};
delete settings.notifications.game; // Remove the deprecated `game`
}
// THIS SHOULD THROW A TYPE ERROR SINCE `car` IS NOT IN GlobalSettings
// AND SHOULD BE REMOVED
// Remove deprecated `car` settings
// if ('car' in settings.notifications) {
// delete settings.notifications.car;
// }
return settings;
}
const DEFAULT_SETTINGS: GlobalSettings = {
notifications: {
system: { enabled: false },
social: { enabled: false },
gameV2: { enabled: false }
}
}
// Usage:
const unsanitizedSettings: UnsanitizedGlobalSettings = {
...DEFAULT_SETTINGS,
notifications: { // Hypothetical user settings loaded from a server or something
game: { enabled: true }, // The deprecated `game` setting is in the server still
car: { enabled: true }, // The deprecated `car` setting is in the server still
system: { enabled: true },
social: { enabled: false }
}
};
// Sanitize and migrate the settings, mutating the original object
const settings = sanitizeAndMigrateSettings(unsanitizedSettings);
// Now, `unsanitizedSettings` is guaranteed to be of type `GlobalSettings`
console.log(settings); // TypeScript should knows this is a `GlobalSettings` now...
/**
* Expected output:
* {
* notifications: {
* system: { enabled: true },
* social: { enabled: false },
* gameV2: { enabled: true }
* }
* }
*/
Any typescript masters want to help make a cool type checking pattern?