typescript - Problema con propiedades genรฉricas cuando se asigna mapeo
generics (4)
Sería genial si pudiera expresar que T no es de tipo Acción. Una especie de inverso de se extiende
Exactamente como dijiste, el problema es que todavía no tenemos restricciones negativas. También espero que puedan obtener esa característica pronto. Mientras espero, propongo una solución como esta:
type KeysOfNonType<A extends object, B> = {
[K in keyof A]-?: A[K] extends B ? never : K
}[keyof A];
// CHANGE: use `Pick` instead of `Omit` here.
type State<Model extends object> = Pick<Model, KeysOfNonType<Model, Action<any>>>;
type Action<Model extends object> = (data: State<Model>) => State<Model>;
interface MyModel<T> {
value: T;
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Now it does exist ๐
data.doSomething; // Does not exist ๐
return data;
}
} as MyModel<any>; // <-- Magic!
// since `T` has yet to be known
// it literally can be anything
}
Tengo una biblioteca que exporta un tipo de utilidad similar al siguiente:
type Action<Model extends object> = (data: State<Model>) => State<Model>;
Este tipo de utilidad le permite declarar una función que se realizará como una "acción".
Recibe un argumento genérico que es el
Model
el cual operará la acción.
El argumento de
data
de la "acción" se escribe con otro tipo de utilidad que exporto;
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
El tipo de utilidad de
State
básicamente toma el
Model
genérico entrante y luego crea un nuevo tipo donde se han eliminado todas las propiedades que son del tipo
Action
.
Por ejemplo, aquí hay una implementación básica de usuario de lo anterior;
interface MyModel {
counter: number;
increment: Action<Model>;
}
const myModel = {
counter: 0,
increment: (data) => {
data.counter; // Exists and typed as `number`
data.increment; // Does not exist, as stripped off by State utility
return data;
}
}
Lo anterior está funcionando muy bien. ๐
Sin embargo, hay un caso con el que estoy luchando, específicamente cuando se define una definición de modelo genérico, junto con una función de fábrica para producir instancias del modelo genérico.
Por ejemplo;
interface MyModel<T> {
value: T; // ๐ a generic property
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Does not exist ๐ญ
data.doSomething; // Does not exist ๐
return data;
}
};
}
En el ejemplo anterior, espero que el argumento de
data
se escriba donde se eliminó la acción
doSomething
y la propiedad de
value
genérico aún existe.
Sin embargo, este no es el caso: nuestra propiedad
State
también ha eliminado la propiedad de
value
.
Creo que la causa de esto es que
T
es genérico sin que se apliquen restricciones / restricciones de tipo, y por lo tanto, el sistema de tipos decide que se cruza con un tipo de
Action
y posteriormente lo elimina del tipo de argumento de
data
.
¿Hay alguna forma de evitar esta restricción?
He investigado un poco y esperaba que hubiera algún mecanismo en el que pudiera afirmar que
T
es cualquiera,
excepto
una
Action
.
es decir, una restricción de tipo negativa.
Imagina:
function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {
Pero esa característica no existe para TypeScript.
¿Alguien sabe de alguna manera que podría hacer que esto funcione como lo espero?
Para ayudar a la depuración, aquí hay un fragmento de código completo:
// Returns the keys of an object that match the given type(s)
type KeysOfType<A extends object, B> = {
[K in keyof A]-?: A[K] extends B ? K : never
}[keyof A];
// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
// My utility function.
type Action<Model extends object> = (data: State<Model>) => State<Model>;
interface MyModel<T> {
value: T; // ๐ a generic property
doSomething: Action<MyModel<T>>;
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
doSomething: data => {
data.value; // Does not exist ๐ญ
data.doSomething; // Does not exist ๐
return data;
}
};
}
Puedes jugar con este ejemplo de código aquí: https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14
Este es un problema muy interesante.
Typecript generalmente no puede hacer mucho con respecto a los parámetros de tipo genérico en tipos condicionales, solo difiere cualquier evaluación de
extends
si encuentra que involucra un parámetro de tipo.
Hay una excepción Si podemos obtener el mecanografiado para usar un tipo especial de relación de tipo, una relación de igualdad no una relación extendida. Una relación de igualdad es fácil de entender para el compilador, por lo que no es necesario diferir la evaluación de tipo condicional. Hay muy pocos lugares en el compilador donde se usa la igualdad de tipos. Uno de los lugares está en restricciones de tipo genérico. Así que veamos un ejemplo:
function m<T, K>() {
type Bad = T extends T ? "YES" : "NO" // unresolvable in ts, still T extends T ? "YES" : "NO"
// Generic type constrains are compared using type equality so this can be resolved inside the function
type Good = (<U extends T>() => U) extends (<U extends T>() => U) ? "YES" : "NO" // "YES"
// If the types are not equal it is still un-resolvable, as K may still be the same as T
type Meh = (<U extends T>()=> U) extends (<U extends K>()=> U) ? "YES": "NO"
}
Podemos aprovechar este comportamiento para identificar tipos específicos.
Ahora, esta será una coincidencia de tipo exacta, no una coincidencia extendida, por lo que podría no ser siempre adecuada, pero dado que
Action
es solo una firma de función, las coincidencias de tipo exacto podrían funcionar lo suficientemente bien.
Entonces, veamos si podemos extraer tipos que coincidan con una firma de función más simple como
(v: T) => void
:
interface Model<T> {
value: T,
other: string
action: (v: T) => void
}
type Identical<T, TTest, TTrue, TFalse> =
((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);
function m<T>() {
type M = Model<T>
type KeysOfIdenticalType = {
[K in keyof M]: Identical<M[K], (v: T) => void, never, K>
}
// Resolved to
// type KeysOfIdenticalType = {
// value: Identical<T, (v: T) => void, never, "value">;
// other: "other";
// action: never;
// }
}
El tipo anterior
KeysOfIdenticalType
está cerca de lo que necesitaríamos para filtrar.
Para
other
, se conserva el nombre de la propiedad, para la
action
se borra el nombre de la propiedad.
Solo hay un problema molesto, alrededor del
value
.
Como el
value
es de tipo
T
no es trivialmente resoluble que
T
y
(v: T) => void
no son idénticos (y de hecho pueden no serlo).
Sin embargo, podemos determinar que el
value
es idéntico a
T
Lo que podemos hacer es que las propiedades de tipo
T
se crucen en esta comprobación para
(v: T) => void
with
never
.
Cualquier intersección con
never
es trivialmente resoluble para
never
.
Luego podemos volver a agregar propiedades de tipo
T
utilizando otra verificación de identidad:
interface Model<T> {
value: T,
other: string
action: (v: T) => void
}
type Identical<T, TTest, TTrue, TFalse> =
((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);
function m<T>() {
type M = Model<T>
type KeysOfIdenticalType = {
[K in keyof M]:
(Identical<M[K], (v: T) => void, never, K> & Identical<M[K], T, never, K>) // Identical<M[K], T, never, K> will be never is the type is T and this whole line will evaluate to never
| Identical<M[K], T, K, never> // add back any properties of type T
}
// Resolved to
// type KeysOfIdenticalType = {
// value: "value";
// other: "other";
// action: never;
// }
}
Entonces la solución final se vería así:
// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object, G = unknown> = Pick<Model, {
[P in keyof Model]:
(Identical<Model[P], Action<Model, G>, never, P> & Identical<Model[P], G, never, P>)
| Identical<Model[P], G, P, never>
}[keyof Model]>;
// My utility function.
type Action<Model extends object, G = unknown> = (data: State<Model, G>) => State<Model, G>;
type Identical<T, TTest, TTrue, TFalse> =
((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);
interface MyModel<T> {
value: T; // ๐ a generic property
str: string;
doSomething: Action<MyModel<T>, T>;
method() : void
}
function modelFactory<T>(value: T): MyModel<T> {
return {
value,
str: "",
method() {
},
doSomething: data => {
data.value; // ok
data.str //ok
data.method() // ok
data.doSomething; // Does not exist ๐
return data;
}
};
}
/// Still works for simple types
interface MyModelSimple {
value: string;
str: string;
doSomething: Action<MyModelSimple>;
}
function modelFactory2(value: string): MyModelSimple {
return {
value,
str: "",
doSomething: data => {
data.value; // Ok
data.str
data.doSomething; // Does not exist ๐
return data;
}
};
}
NOTAS: La limitación aquí es que esto solo funcionará con un parámetro de tipo (aunque posiblemente se pueda adaptar a más). Además, la API es un poco confusa para cualquier consumidor, por lo que esta podría no ser la mejor solución. Puede haber problemas que aún no he identificado, si encuentra alguno, hágamelo saber ๐
Generalmente leo eso dos veces y no entiendo completamente lo que quieres lograr.
Según tengo entendido, desea omitir la
transform
del tipo que se da para
transform
exactamente.
Para lograr eso es simple, necesitamos usar
Omit
:
interface Thing<T> {
value: T;
count: number;
transform: (data: Omit<Thing<T>, ''transform''>) => void; // here the argument type is Thing without transform
}
// ๐ the factory function accepting the generic
function makeThing<T>(value: T): Thing<T> {
return {
value,
count: 1,
transform: data => {
data.count; // exist
data.value; // exist
},
};
}
No estoy seguro de si esto es lo que quería debido a la gran complejidad que ha dado en los tipos de utilidad adicionales. Espero eso ayude.
count
y
value
siempre harán que el compilador sea infeliz.
Para solucionarlo, puede intentar algo como esto:
{
value,
count: 1,
transform: (data: Partial<Thing<T>>) => {
...
}
}
Como se está utilizando el tipo de utilidad
Partial
, estará bien en el caso de que el método de
transform
no esté presente.