Fallo de inferencia de tipo de unión en TypeScript
Parece que a veces TypeScript (de la versión reciente) no logra acotar los tipos de unión incluso si hay una guardia de tipos presente. ¿Es este comportamiento un error o una característica:
Preámbulo :
// some config
interface Config {
name: string;
option1?: number;
option2?: boolean;
}
// arbitrary type
interface Entity {
a: number;
b: number;
}
// name aware type guard for entity property-to-config map
// some default config may be replaced with a property name
type TConfigSet<TData> = {
[P in keyof TData]: (Config & { name: P }) | P;
}
// example of TConfigSet usage
const EntityConfigs: TConfigSet<Entity> = {
a: {
name: 'a',
option2: true
},
b: 'b'
}
Pregunta :
// this function compiles
function TypeLooseFieldToName(name: string | Config): string {
if (typeof name === 'string') return name;
else return name.name;
}
// this one doesn't
function TypeStrictFieldToName<TData>(name: keyof TData | { name: keyof TData }): keyof TData {
if (typeof name === 'string') return name;
else return name.name; // still keyof TData | { name: keyof TData }, should be shrinked to { name: keyof TData }
}
Respuesta #1
Parece ser un error en el verificador de tipos, porque el manual de TypeScript dice "un tipo keyof T se considera un subtipo de string."
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html
Como solución alternativa, la protección de tipo se puede invertir para descartar primero el tipo personalizado:
function hasName(obj: string | { name: string }): obj is { name: string } {
return typeof obj.name === 'string';
}
function getName<TData>(name: keyof TData | { name: keyof TData }): keyof TData {
if (hasName(name)) return name.name;
else return name;
}
// compiles with valid keys
getName<Entity>('a');
getName<Entity>({ name: 'a' });
// doesn't compile with invalid keys
getName<Entity>('z');
getName<Entity>({ name: 'z' });
Podrías buscar los problemas de TypeScript en GitHub y crear un nuevo problema si esto no se ha abordado previamente:
https://github.com/Microsoft/TypeScript/issues
Respuesta #2
typeof name === 'string' no funciona como guardia de tipo para keyof TData porque aparentemente string y keyof TData son asumidos como dos tipos diferentes por el compilador. Si agregas tu propio guardia de tipo personalizado para keyof TData, funciona:
function TypeStrictFieldToName<TData>(name: keyof TData | { name: keyof TData }): keyof TData {
if (isKeyofTData<TData>(name)) return name;
else return name.name; // type of name here is { name: keyof TData }
}
function isKeyofTData<TData>(name: keyof TData | {}): name is keyof TData {
return typeof name === 'string';
}
