Skip to content

Instantly share code, notes, and snippets.

@Taytay
Created May 31, 2020 19:50
Show Gist options
  • Save Taytay/7d79eb02c28f62d704e93a03e95e7b2d to your computer and use it in GitHub Desktop.
Save Taytay/7d79eb02c28f62d704e93a03e95e7b2d to your computer and use it in GitHub Desktop.
// Here is the link to the playground for the following code:
// https://www.typescriptlang.org/play?#code/MYGwhgzhAEAiD2BzaBvAUNT0wDsCWAtmCABKQBi8ArgE4AyApojALzQAuNVDA3BlgAcqAIxB5g0ACZIAcmAIMAXNAic8OZGwBEWvlmhDR46OrzsAFALA15EZQkQAFa7YCUqfvszB4OCPBAGADoQJHMAcgBJbAJsKSQAGmwcSWhoxHh2DgALBggGAxcCO2hw6ABqQptioOlEADViblc9LABfNA7QSBgAYTAs9H1cQmIyCEpaRmZoNk5uVsxDMQlgAbkFZVUadU1oHUWDERWTfAsrapL+9mdL9yGvb19-QJCwqJi4tfYk3FT0zI5PIFC62ZRlSqgmrfRogZqHDpdcBQOBIW62Dz6ZbGOqw7jKHBUAjCBg0WbQAAMhxGRFIFGo9CYrA4XAYnTQ3RR13RxUxgmOxhhTSU0EJxNJ5KpnmSozpEwZ02Z8zZHTQDDF0AA0upUg9MA5tSlyeEDTrwglpddDak2OErWaLaqfH4sgBrHUAWTAAnJeugAG1rUFTSkALr2RLSwM6oL2sPKa6OvjsACeAgK1q9PrmaYY8AAZtB3Sks3w0Kn09AACpUASBCAAHk15OtAD4W9AGAAPdjqyQwa3QAD8AelmqS0siLtwwAYVdzTc93sDodbE-0U9UM7nC-MFbzheLkizrhX7e7vZSMGl+hQpzMynMYGU6nzEsc7hY7dwKegbWH0CONABIMAAbqSaChsBopgaSZYAPTwdAzqqFUthZr6aCIV40YpMGSDWuGqJOEUEAWth+i4ZIsYDIRCYDDyZFYUhbQIUh+6AaRGE5umBZocUpZoMx0AkKSBRmCYMAAFZUKhsm7ECnGXFmwkcTWdZ5E2LY6u2bDNhefYDjqAH+sJ+jjmZWCbuw27zumi4lsumqruRSEbtOOCznZDANoxWZnsJUEgeBNBluWubQL0yIQN5WkGVeWo6eSACiPY2MA7ANup9ZJIGvw4Cm+Upqu-oAIyhsmEWMbF+k9oZiUpLp0CpZwYAZVltY5QG47JIVvUlQATBVQkcQAqvgvhVvAU69jQ+QZXgvgNqNTXmKNnZ1QlP4AeYrrKKNn7tqB8B4KkwWku48X9tA5i7S+OBvmSkSHdAx2ne4I7ROdNDhZW5CeVl5LZXkTXjYtOBTTNpLzew4MNtKVYbZe13AzAI67QwKbKFW-oUqGSQCNj-pDS9OPldBOCwT9rZ8ChWSIAw7AAIL4LS1nbso-0SGw+ZUJ5sO+NAcWbddbYY1jWoE6RyjVQumqtq4CbRTV7Z+nTMq0pq0Xkke-mupjw3SurNLEOznkFGwlMAO4a8QWs9OYLTStKJsgGbs5BKY5ykU70o0IztA4LbbsebOfCqthVvwDQrpkdATMyLA0DZJA0AZPAqTqL2IBiPkfgMAAhByzxZN85IM8zrOm6HDDmEGcaSEk1uRQxpGO772FVrkLLUKI4lxwySm2CrySpFFPQj6E8Cumn8AZ9A+Y0PAsTsN3AD61DsBAp0MGv0B8avBS8-z4NJMIVBZIfv5tewVDECA185-ANv5tHUi4AzS+yap2S7DAkQyAAMqRFgMlfehZD4Lz5gtXwZ8GBrFkgUfcMBkHQH9i8cCHB4C9SCMJRwS8CyKDQMfGBQcK4s1lO7BgA1hbIyMo1cWygepQhKLLey8tFaRWVnLVWRsS7B3tiiNgutlz62KocY2VcQ5bnNuSZurtBEQEdtSKRVDPZnEsD7Ms+hO6-xgBAbI1AQCpBJMkTsNAl40CIfodWGoCREhJGSNgLDDjSl0XgGAHizGkksflTOWR-bgWIDAK2KcsgAAMWHhOwP7ZOYllA-jcW5asEVwgtXSplKi+FEDWiSA4PJaJSL426kVIqg0oIAB9mppRvg2LJDckiJhbjcIpuUeo-jKa2YmoYyheJwICHoeBEA4DAH3LBHBUmUxCuEXBOikLeVKOk2pWSQyN2IgUkilxil5V6p07pvSYD9KyIM4ZozAjjI4uEKZpIZlJKwAs8IP4DmigGVAIZIyxnsGwZc65NBbk2P4QIAhhZvrkhcc7OZ1Y9EqEMVQYx0BTG4HMZY6xWB-a3xoEHAArOHIS2FAHwCSIYm20hoBWyPngLsOQPFDhGhFQBYBHrjygDVJG9U2wpRqe1VGbSNnQCqU0hwjF+XNMYiVcqlVKwMsemwnytU6ENUkE1JZ3LOp5F5cREVgrCmXC1a3LZXShpsWgAAdQYDnJI5LkJItJavDxZ8L5koYOEWJwg2ozy+WS3IsSrVbhoL2SQRCSECzIYzChbMa4DSxQ2JmbKEpizEcoJmUtLjKGlaSWVMaFZpsZaSZlMUFxM14QCl0AjtbCKXAIf0YjsAwG2qnQiEj+GuyoXIhgNsFHRWUXcyIWQZKoVdP0q2KCwn8U8TAby5Al4EGBgADizUENIWRpB5BwOEAJDBiB4AAF7iXYOEOtooHESnUHE-2SRYJB1XtQRA2QgSxOsAUXwD8OBRwMPAN5YzQLChgHxJmdyABCjqh3UvHd4ix0dUWYE7qkoVpERXmG5PBgAZMRRirhnlHNrdvU5nzvmpKQ5cf5WAKKPEwPg+A6Z-W-nCEKOEzrJLQAIB47eGhTgTMrCaHVtgyjnw3QARyoHgf2mcr0Ef1Tx2ZWAW013UWYTRdxXGQpZqkfwjG7V2DuQ8hwmHXk4Y+ecz1lzrgIYcNAVD1wMNSeg8k7TSBdPHLebhwz+HONmYswMYjNmvAUao6mUo3wNgMa8cxt5bHT2XJ0wix1-tBPCYYKJjjBQ7Sees2ggOmLg5UNxcJXovhQDyV8NjbuKrL47GADPRAYBMGyTiFbMAv5PUp0wT4Cx8CshT2LMgYN4MVDvIGLQPI+8L7b0kE+iB3ceuwK9Xgc56CAigQUpA1Bnrwk-miX8bAT8rYKR-HatjTXvTpiDuoUb43FJTZwIuruAxsAZTvjnX8XijxgGQnPEADqsgQDAJIF9WHU7fbfLWp1ZKjGpGawUMQ+tqJ4qQtEVQs2QDxDXW6EDr36uNewUQfWoGVC5poEEIAA
class Dog {
animalHasFourLegs = true;
public dogName: string = "";
public init(params: DogParams) {
console.log('I am a dog, and I got these params: ' + params.dogValue);
}
}
class Cat {
animalHasFourLegs = true;
public catName: string = "";
public init(params: CatParams) {
console.log('I am a cat, and I got these params: ' + params.catValue);
}
}
class DogParams {
public dogValue: number = 0;
animalHasFourLegs = true
}
class CatParams {
public catValue: number = 0;
animalHasFourLegs = true
}
enum Kind {
DogKind = 'DogKind',
CatKind = 'CatKind',
}
const kindMap = {
[Kind.DogKind]: Dog,
[Kind.CatKind]: Cat,
};
type KindMap = typeof kindMap;
type Tuples<K = Kind> = K extends Kind ? [
K,
InstanceType<KindMap[K]>,
InstanceType<(typeof kindMap)[K]> extends
{ init: (a: infer P) => any } ? P : never
] : never;
// const paramsMap = {
// [Kind.DogKind]: DogParams,
// [Kind.CatKind]: CatParams,
// };
// type ParamsMap = typeof paramsMap;
// Here it is just using the ParamsMap
// type Tuples<K = Kind> = K extends Kind ? [
// K,
// InstanceType<KindMap[K]>,
// InstanceType<ParamsMap[K]>
// ] : never;
type ClassType<K extends Kind> = Extract<Tuples, [K, any, any]>[1];
type ParamsType<K extends Kind> = Extract<Tuples, [K, any, any]>[2];
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type Fnc<T = Tuples> = UnionToIntersection<
T extends Tuples ? (key: T[0], p: T[2]) => T[1] : never
>;
const getAnimalInstance: Fnc = function <K extends Kind>(key: K, params: ParamsType<K>): ClassType<K> {
const animalKlass = kindMap[key];
const animalInstance = new animalKlass();
animalInstance.init(params);
return animalInstance;
}
// works, AND has good intellisense!
const cat = getAnimalInstance(Kind.CatKind, new CatParams());
// The trouble is, our ParamsType<K> and ClassType<K> look good from the _outside_ of the function, but they actually allow for dangerous
// things INSIDE of the function, because types types resolve to any.
// Proof:
function getAnimalInstance2<K extends Kind>(key: K, params: ParamsType<K>): ClassType<K> {
const animalKlass = kindMap[key];
const animalInstance = new animalKlass();
animalInstance.init(params);
// This should be an error:
const num : number = params;
// This is an error, and it reveals what `params` are here: any
// Type 'Extract<[Kind.DogKind, Dog, DogParams], [K, any, any]>[2] | Extract<[Kind.CatKind, Cat, CatParams], [K, any, any]>[2]' is not assignable to type 'never'.
// Type 'Extract<[Kind.DogKind, Dog, DogParams], [K, any, any]>[2]' is not assignable to type 'never'.
// Type 'any' is not assignable to type 'never'.
const proof : never = params;
// This should be an error:
return 5;
}
// So, how do we fix this?
type SaferClassType<K extends Kind> = Extract<Tuples, [K, Dog | Cat, DogParams | CatParams]>[1];
type SaferParamsType<K extends Kind> = Extract<Tuples, [K, Dog | Cat, DogParams | CatParams]>[2];
// Well, we can do this, but we're back to where we started:
function getAnimalInstance25<A extends Kind>(key: A, params: SaferParamsType<A>): SaferClassType<A> {
const animalKlass = kindMap[key as any as Kind];
const animalInstance = new animalKlass();
// It just knows that params is TypeFromTuple8<A>. It doesn't realize it's a number in here, even though there are only two possible values of A
// But now this is an error:
// Type 'DogParams | (CatParams & DogParams)' is not assignable to type 'CatParams'.
// Property 'catValue' is missing in type 'DogParams' but required in type 'CatParams'.
animalInstance.init(params);
// And so is this:
// Type 'Dog' is not assignable to type 'Cat | (Dog & Cat)'.
// Type 'Dog' is not assignable to type 'Dog & Cat'.
// Property 'catName' is missing in type 'Dog' but required in type 'Cat'.
return animalInstance;
}
// Conclusion: The Extract trick gave us a way to have correct looking function signatures outside of the function, while resolving the types to `any` and allowing anything to happen inside of the function. That actually is kinda cool, but sadly not as safe as we would have liked.
// I still don't know a way to make this safer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment