It is assumed that the literal types of the stringified values are not known at compile time. This means that dealing with TypeScript code in which the compiler validates whether a string literal conforms to a specific type is not expected.
// Example of what is not expected
const stringifiedThing: SomeType = "{\"prop1\": \"value1\", \"prop2\": \"value2\"}";
If the assumption is incorrect and validation at compile time is necessary, template literal types could be explored. However, implementing parsers with template literal types can be complex. There may not be a specific type that accurately represents SomeType
using pattern template literal types.
type SomeType = `{"prop1": "${string}", "prop2": "${string}"}`;
Although this approach has limitations, such as accepting incorrectly formatted string literals, it could be sufficient in certain scenarios. Nonetheless, creating a comprehensive Stringified<T>
type for all T
inputs would be challenging.
For cases where runtime stringifications result in the stringified values instead of hardcoded values in TypeScript code, a compile-time string parser may have limited usefulness.
If compile-time validation is not required, having Stringified<T>
function as a nominal subtype of string
dependent on T
could be a suitable alternative. While TypeScript lacks direct nominal types, simulating them using structural typing and object intersection with the string
type can achieve the desired effect.
type Stringified<T> = string & { __structure: T }
By introducing a phantom property __structure
in Stringified<T>
, the compiler enforces restrictions on assigning random strings to this type. At runtime, this property does not exist, making it a compiler-only feature.
Type assertions are required when assigning a string
to such types. For instance, a stringify()
function can handle this task to prevent haphazard assignments:
function stringify<T>(t: T): Stringified<T> {
return JSON.stringify(t) as Stringified<T>; // assertion needed
}
Additionally, a parse()
function can be implemented to convert a Stringified<T>
back into a T
. Although type safety isn't guaranteed due to the limitations of JSON.parse()
, this approach enables stringified type tracking by the compiler.
function parse<T>(s: Stringified<T>): T {
return JSON.parse(s); // type checks but not validated
}
These functions facilitate the parsing and validation of stringified values, ensuring the expected types are retained:
const foo = stringify({ a: 1, b: "two" });
const val = parse(foo);
console.log(val.b.toUpperCase()); // type-aware access
Attempts to parse a random string will be rejected, reinforcing the type safety of the implementation:
parse("oopsie"); // error! Argument of type 'string' is not
// assignable to parameter of type 'Stringified<unknown>'.
The proposed approach provides a robust solution for representing stringified objects in TypeScript, emphasizing type safety and clarity in handling stringified values.
Link to Playground