Skip to content

Instantly share code, notes, and snippets.

@SamPruden
Last active August 7, 2017 19:38
Show Gist options
  • Select an option

  • Save SamPruden/ab0c7305d241cee4c7f0452f11a4d1f1 to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Author

Oh, arrays. I'm working on it.

@SamPruden
Copy link
Copy Markdown
Author

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

@SamPruden
Copy link
Copy Markdown
Author

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

@SamPruden
Copy link
Copy Markdown
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