-
-
Save SamPruden/ab0c7305d241cee4c7f0452f11a4d1f1 to your computer and use it in GitHub Desktop.
// WHAT IS THIS? | |
// This is me playing around with a cool (but dirty) trick | |
// Some interfaces can be globally augmented to provide info to the type system | |
// This then allows some basic type switch and retrieval operations | |
// I needed a DeepReadonly<T> for a thing, I think I have it now | |
// Built/tested in 2.5.0-dev20170803 | |
// FUTURE PLANS | |
// - This needs lots of tidying and renaming | |
// - Do more stuff | |
// - Make better plans | |
// - Lots of refactoring | |
// - Seriously, can we at least have things named slightly consistently? | |
// String unions for typeof | |
type PrimitiveTypeString = "string" | "number" | "boolean" | "symbol"; | |
type NonPrimitiveTypeString = "object" | "function"; | |
type TypeString = PrimitiveTypeString | NonPrimitiveTypeString; | |
// There's an unnecessary level here for now, but I have plans to add some other things to !_typeinfo | |
// The leading ! prevents this from showing up in intellisense | |
// Could actually just add the property to the prototype so that this isn't a lie | |
interface WithTypeInfo<T extends TypeString = TypeString> { | |
/** | |
* Ghost property used for doing type operations, does not actually exist | |
*/ | |
readonly "!_typeInfo": { | |
readonly type: T; | |
// readonly specialType?: string; | |
} | |
} | |
// Globally extend these interfaces with typing annotations | |
interface Object extends WithTypeInfo<"object"> { } | |
interface Function extends WithTypeInfo<"function"> { } | |
interface String extends WithTypeInfo<"string"> { } | |
interface Number extends WithTypeInfo<"number"> { } | |
interface Boolean extends WithTypeInfo<"boolean"> { } | |
interface Symbol extends WithTypeInfo<"symbol"> { } | |
// Special type info provided for arrays | |
// Maybe generalise this for various things with various type parameters | |
interface Array<T> { // extends WithTypeInfo<"object"> { | |
readonly "!_typeInfo": { | |
readonly type: "object"; | |
readonly specialType: "array"; | |
readonly arrayType: T; | |
} | |
} | |
// Fetch stuff please stuff - this is a poor comment | |
type TypeInfo<T extends WithTypeInfo> = T["!_typeInfo"]; | |
type TypeOf<T extends WithTypeInfo> = TypeInfo<T>["type"]; | |
// A couple of type operation utilities | |
// Stolen, ahem, borrowed, from tycho01's typical repo | |
type Obj<T> = { [k: string]: T }; | |
type UnionHasKey<Union extends string, K extends string> = ({[S in Union]: '1' } & Obj<'0'>)[K]; | |
// Typeof operations | |
type Is<T extends WithTypeInfo, TypeUnion extends TypeString> = UnionHasKey<TypeUnion, TypeOf<T>>; | |
type IsPrimitive<T extends WithTypeInfo> = Is<T, PrimitiveTypeString>; | |
// For accepting only primative/non-primative arguments | |
type Primitive = WithTypeInfo<PrimitiveTypeString>; | |
type NonPrimitive = WithTypeInfo<NonPrimitiveTypeString>; | |
// Get the type of an array | |
type TypeOfArray<T extends Array<any>> = T["!_typeInfo"]["arrayType"]; | |
// Unsafe type, trusts that T is an array, returns never if not | |
type TypeOfArrayUnsafe<T extends WithTypeInfo> = (Obj<never> & T["!_typeInfo"])["arrayType"]; | |
type SpecialTypeOf<T> = (TypeInfo<T> & Obj<"NONE">)["specialType"]; | |
type HasSpecialType<T> = ({"NONE": "0"} & Obj<"1">)[SpecialTypeOf<T>]; | |
type IsSpecial<T, S extends string> = (Obj<"0"> & {[K in S]: "1"})[SpecialTypeOf<T>]; | |
type IsArray<T> = IsSpecial<T, "array">; | |
// I should move other things over to use this | |
type If<C extends "0" | "1", T, F> = {"0": F; "1": T;}[C]; | |
// Not currently used | |
// To do: Investigate tuples | |
type DeepReadonlyArray<T extends any[]> = ReadonlyArray<DeepReadonly<TypeOfArray<T>>>; | |
// Trusts T to be an array, treats as Array<any> if not | |
type DeepReadonlyArrayUnsafe<T> = ReadonlyArray<DeepReadonly<TypeOfArrayUnsafe<T>>>; | |
// Recursion, makes all properties deep readonly | |
type DeepReadonlyObject<T extends WithTypeInfo> = {readonly [K in keyof T]: DeepReadonly<T[K]>}; | |
// Takes any type, including primitives, and makes them deep readonly | |
type DeepReadonly<T extends WithTypeInfo> = { | |
"object": If<IsArray<T>, DeepReadonlyArrayUnsafe<T>, DeepReadonlyObject<T>>; | |
"number": T; | |
"function": T; | |
"string": T; | |
"boolean": T; | |
"symbol": T; | |
}[TypeOf<T>]; |
Should this be done in production? Uhh... I'm going to try it, let's see how badly it backfires. If the issue mentioned above is not a bug, then this is a little hackish, but valid TypeScript.
Even if DeepReadonly<T>
does rely on a bug, I think there may be a solution with optional properties, that needs investigation.
Oh, arrays. I'm working on it.
Arrays are proving tricky, but I'm 95% sure that is because of a bug in type declarations.
Updated DeepReadonly<T>
to handle more stuff, namely to smoothly handle arrays.
I fear DeepReadonly<T>
as implemented here may only work because of a bug like #17456. I think I may be able to fix that by removing some of the <T extends WithTypeInfo>
requirements and providing fallback values if T
doesn't extend WithTypeInfo
.
I'm a little worried that the
DeepReadonly<T>
implementation may rely on a bug. Should the nestedDeepReadonly<T[K]>
be valid?T[K]
isn't guaranteed to matchWithTypeInfo
.