Skip to content

Instantly share code, notes, and snippets.

@SamPruden
Last active August 7, 2017 19:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save SamPruden/ab0c7305d241cee4c7f0452f11a4d1f1 to your computer and use it in GitHub Desktop.
Save SamPruden/ab0c7305d241cee4c7f0452f11a4d1f1 to your computer and use it in GitHub Desktop.
Messing around with a trick that allows basic type switching in TypeScript, and provides the ability to make DeepReadonly<T>
// 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>];
@SamPruden
Copy link
Author

I'm a little worried that the DeepReadonly<T> implementation may rely on a bug. Should the nested DeepReadonly<T[K]> be valid? T[K] isn't guaranteed to match WithTypeInfo.

@SamPruden
Copy link
Author

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.

@SamPruden
Copy link
Author

Even if DeepReadonly<T> does rely on a bug, I think there may be a solution with optional properties, that needs investigation.

@SamPruden
Copy link
Author

Oh, arrays. I'm working on it.

@SamPruden
Copy link
Author

Arrays are proving tricky, but I'm 95% sure that is because of a bug in type declarations.

@SamPruden
Copy link
Author

Updated DeepReadonly<T> to handle more stuff, namely to smoothly handle arrays.

@SamPruden
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment