Skip to content

Instantly share code, notes, and snippets.

@ivawzh
Last active February 26, 2024 07:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ivawzh/af3d3bcd9f9f1bfa82f49c523d6c3672 to your computer and use it in GitHub Desktop.
Save ivawzh/af3d3bcd9f9f1bfa82f49c523d6c3672 to your computer and use it in GitHub Desktop.
Typescript crazy mapped types
// This is only working in 4.1.0-insiders20200903
type ParserError<T extends string> = { error: true } & T
type EatWhitespace<State extends string> =
string extends State
? ParserError<"EatWhitespace got generic string type">
: State extends ` ${infer State}` | `\n${infer State}`
? EatWhitespace<State>
: State
type AddKeyValue<Memo extends Record<string, any>, Key extends string, Value extends any> =
Memo & { [K in Key]: Value }
type ParseJsonObject<State extends string, Memo extends Record<string, any> = {}> =
string extends State
? ParserError<"ParseJsonObject got generic string type">
: EatWhitespace<State> extends `}${infer State}`
? [Memo, State]
: EatWhitespace<State> extends `"${infer Key}"${infer State}`
? EatWhitespace<State> extends `:${infer State}`
? ParseJsonValue<State> extends [infer Value, `${infer State}`]
? EatWhitespace<State> extends `,${infer State}`
? ParseJsonObject<State, AddKeyValue<Memo, Key, Value>>
: EatWhitespace<State> extends `}${infer State}`
? [AddKeyValue<Memo, Key, Value>, State]
: ParserError<`ParseJsonObject received unexpected token: ${State}`>
: ParserError<`ParseJsonValue returned unexpected value for: ${State}`>
: ParserError<`ParseJsonObject received unexpected token: ${State}`>
: ParserError<`ParseJsonObject received unexpected token: ${State}`>
type ParseJsonArray<State extends string, Memo extends any[] = []> =
string extends State
? ParserError<"ParseJsonArray got generic string type">
: EatWhitespace<State> extends `]${infer State}`
? [Memo, State]
: ParseJsonValue<State> extends [infer Value, `${infer State}`]
? EatWhitespace<State> extends `,${infer State}`
? ParseJsonArray<EatWhitespace<State>, [...Memo, Value]>
: EatWhitespace<State> extends `]${infer State}`
? [[...Memo, Value], State]
: ParserError<`ParseJsonArray received unexpected token: ${State}`>
: ParserError<`ParseJsonValue returned unexpected value for: ${State}`>
type ParseJsonValue<State extends string> =
string extends State
? ParserError<"ParseJsonValue got generic string type">
: EatWhitespace<State> extends `null${infer State}`
? [null, State]
: EatWhitespace<State> extends `"${infer Value}"${infer State}`
? [Value, State]
: EatWhitespace<State> extends `[${infer State}`
? ParseJsonArray<State>
: EatWhitespace<State> extends `{${infer State}`
? ParseJsonObject<State>
: ParserError<`ParseJsonValue received unexpected token: ${State}`>
type ParseJson<T extends string> =
ParseJsonValue<T> extends infer Result
? Result extends [infer Value, string]
? Value
: Result extends ParserError<any>
? Result
: ParserError<"ParseJsonValue returned unexpected Result">
: ParserError<"ParseJsonValue returned uninferrable Result">;
type Json = ParseJson<'{ "key1": ["value1", null], "key2": "value2" }'>;
// type Json = {
// key1: ["value1", null];
// } & {
// key2: "value2";
// }
import { $Keys, PickByValue, OmitByValue } from 'utility-types'
type PickByPossibleValue<T, Targets> = OmitByValue<{
[K in keyof T]: Extract<T[K], Targets> extends never ?
never
:
T[K]
}, never>
type OmitByPossibleValue<T, Targets> = Omit<T, $Keys<PickByPossibleValue<T, Targets>>>
type NonNullableOrUndefinableKeys<T> = $Keys<OmitByPossibleValue<T, null | undefined>>
type NullableOrUndefinableKeys<T> = $Keys<PickByPossibleValue<T, null | undefined>>
type A = {
a: null | 1,
b: null | 2,
c: undefined | 3,
d: undefined | 4,
e: undefined | null | 5,
f: 6,
g: null,
h: undefined,
}
type Case0 = PickByPossibleValue<A, null | undefined>
// type Case0 = {
// a: null | 1;
// b: null | 2;
// c: undefined | 3;
// d: undefined | 4;
// e: undefined | null | 5;
// g: null;
// h: undefined;
// }
type Case1 = OmitByPossibleValue<A, null | undefined>
// type Case1 = {
// f: 6;
// }
type Case2 = NonNullableOrUndefinableKeys<A>
// type Case2 = "f"
type Case3 = NullableOrUndefinableKeys<A>
// type Case3 = "a" | "b" | "c" | "d" | "e" | "g" | "h"
// [Playground Link](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgEgNIFMCeBnANHABWAGMBrAIUwDUBDAGwFd18B5EYGS2x9OAXzgAzKBBBwA5AxjA6HTAFoYmMOmziAsACgtSlYRIVMBCNmzAARnXTcmAHgAq+ezSgBzdDGwA+OAF44bBxc9HYIWnBwANqocMAAdnCkWBCCcPYAugBccACiAB4wUDTEMA7R6U4u7p4+6AXocQAm2HBx6ABu6FBwAPzhEa0dXf2Z-RH25Vp8+G2dUF5aOsq8gZxGJmaW1iHoDpVuHt5+AeyljigYOLZEZJTGphZWNrvnzgc1Xgvamrq8AHIQOJ-Bh0Og0LYsKAAVSa6EE8XBVku2AcPn8aCwKNWdw2j22PD2rRBdDgAB84AxYfC2o1Posfss4MDQYj0JCYY04QitsjUccMVcboZ7psnjtCXFiWSKVT4uhaV8lnoAILHMKaCI0bKS0HSgCMuH65m1UvJACZDRq4MRspTOdT5dKAMyWiKNW2ymnSgAsrrg6A99rljWlOpJ5IArH7BNkAGx+1wm0F+gAWga5NMtfHpvzgAGEaNh0AAGY5CnEPLbPWzKmammVBmlfAD0zbguYLRdL-nVrYGmqT4bgeoA3Fo+-3jUTdeax5oJwMbQ2M47yU65wu3emHSHyd6N23+wHlzvQ-WIwf+3BE9O6Jf+2mT8HL9nvh3C+g9cdsetK2KCbWt7SnaK60uObbvkWX49uBV4xnAsYvjmjKdugZrHACQLEqy7KeqyvLKi2EEoR+6H+AARII5HIXoqFOhh2EQtCeE8piNZEe2JFFvRFE0OR0rkeY-HkuRxDCXA5GNOJ5HoNJrjSSm1HfFoQA)
const shallow = {
date: new Date(),
num: 1,
n: null,
u: undefined,
s: 'a',
b: true,
}
type ShallowObjectTransform<
O extends Record<string, unknown>,
FromType,
ToType,
> =
Overwrite<O, {
[K in $Keys<PickByValue<O, FromType>>]: ToType
}>
const shallowDateTest: ShallowObjectTransform<typeof shallow, Date, string>['date'] = 'string' // ✅
const leaveInnocentAloneTest: ShallowObjectTransform<typeof shallow, Date, string>['num'] = 1 // ✅
import { $ElementType, PromiseType } from 'utility-types'
/**
* @desc Universal transformation mapped type.
* - Support scalar, union, object, array, tuple, flat, or nested within original type.
* - Support multiple transformations in one declaration.
* @example
* UniversalTransform<Original, [From, To] | [Date, string] | ... >
*
* type R0 = UniversalTransform<Date | number, [Date, string] | [number, boolean]>
* // R0 = string | boolean
* type R1 = UniversalTransform<{date: Date, innocent: number}, [Date, string]>
* // R1 = { date: string, innocent: number }
* type R2 = UniversalTransform<{deep: {date: Date}}, [Date, string]>
* // R2 = { deep: { date: string } }
* type R3 = UniversalTransform<Date[], [Date, string]>
* // R3 = string[]
* type R4 = UniversalTransform<Date[[]], [Date, string]>
* // R4 = string[][]
* type R5 = UniversalTransform<Date, [Date, string]>
* // R5 = string
*/
type UniversalTransform<T, FromTo extends [any, any]> =
T extends FromTo[0] ?
ShallowTransform<T, FromTo>
:
{
[K in keyof T]: T[K] extends FromTo[0] ?
ShallowTransform<T[K], FromTo>
: T[K] extends (infer R)[] ?
UniversalTransform<R, FromTo>[]
: T[K] extends object ?
UniversalTransform<T[K], FromTo>
: Extract<T[K], FromTo[0]> extends FromTo[0] ?
UnionTransform<T[K], FromTo>
:
T[K]
}
type ShallowTransform<T, FromTo extends [any, any]> =
FromTo extends any ?
[T] extends [FromTo[0]] ?
FromTo[1]
:
never
:
never
type UnionTransform<T, FromTo extends [any, any]> =
| UniversalTransform<Extract<T, object>, FromTo>
| Exclude<T, FromTo[0] | object>
| FromTo[1]
// ----------------------------------- Specs ------------------------------------
const obj = {
number: 1,
date: new Date(),
deep: { date: new Date() },
arrayDeep: [{ date: new Date() }],
array: [new Date()],
tuple: [new Date(), 2, true],
tupleWithObj: [{ date: new Date() }, 2, 'hi', { hello: 'world' }],
tupleWithTuple: [[1, false], [2, new Date()], [3, { date: new Date() }]]
}
type ArrayType<A extends unknown[]> = $ElementType<A, number>
const date = new Date()
const number = 2
const n = null
const nestedArray = [[[new Date()]]]
const scalarTest: UniversalTransform<typeof date, [Date, string]> = 'string' // ✅
const constTest: UniversalTransform<typeof number, [Date, string]> = 2 as const // ✅
const primitiveTest: UniversalTransform<typeof n, [Date, string]> = null // ✅
const nestedArrayTest: UniversalTransform<typeof nestedArray, [Date, string]> = [[['string']]] // ✅
let o: UniversalTransform<typeof obj, [Date, string]>
const innocentTest: typeof o.number = 2 // ✅
const shallowTest: typeof o.date = 'string' // ✅
const deepTest: typeof o.deep.date = 'string' // ✅
const arrayTest: ArrayType<typeof o.array> = 'string' // ✅
const arrayObjTest: ArrayType<typeof o.arrayDeep>['date'] = 'string' // ✅
const tupleTest: typeof o.tuple = ['string'] // ✅
const tupleObjTest: typeof o.tupleWithObj = [{ date: 'string' }] // ✅
const tupleTupleTest: typeof o.tupleWithTuple = [[1, false], [2, 'string'], [3, { date: 'string' }]] // ✅
const unionTest: UniversalTransform<Date | number, [Date, string] | [number, boolean]>[] = ['string', false] // ✅
// ---------------------------- NextJs GSSP ----------------------------------
const gssp = async () => ({ props: obj })
type PropsFromGSSP = UniversalTransform<
PromiseType<ReturnType<typeof gssp>>['props'],
[Date, string]
>
/**
* type PropsFromGSSP = {
number: UnionTransform<number, [Date, string]>;
date: string;
deep: {
date: string;
};
arrayDeep: {
date: string;
}[];
array: string[];
tuple: (string | number | boolean)[];
tupleWithObj: (string | number | {
date: string;
hello?: UnionTransform<undefined, [Date, string]>;
} | {
hello: string;
date?: UnionTransform<undefined, [Date, string]>;
})[];
tupleWithTuple: ((string | number | boolean)[] | UnionTransform<number, [Date, string]>[] | UnionTransform<number | {
date: Date;
}, [Date, string]>[])[];
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment