Typescript: Tipos exactos

Creado en 15 dic. 2016  ·  171Comentarios  ·  Fuente: microsoft/TypeScript

Esta es una propuesta para habilitar una sintaxis para tipos exactos. Se puede ver una característica similar en Flow (https://flowtype.org/docs/objects.html#exact-object-types), pero me gustaría proponerla como una característica utilizada para literales de tipo y no para interfaces. La sintaxis específica que propondría usar es la tubería (que casi refleja la implementación de Flow, pero debería rodear la declaración de tipo), ya que es familiar como la sintaxis absoluta matemática.

interface User {
  username: string
  email: string
}

const user1: User = { username: 'x', email: 'y', foo: 'z' } //=> Currently errors when `foo` is unknown.
const user2: Exact<User> = { username: 'x', email: 'y', foo: 'z' } //=> Still errors with `foo` unknown.

// Primary use-case is when you're creating a new type from expressions and you'd like the
// language to support you in ensuring no new properties are accidentally being added.
// Especially useful when the assigned together types may come from other parts of the application 
// and the result may be stored somewhere where extra fields are not useful.

const user3: User = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Does not currently error.
const user4: Exact<User> = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Will error as `foo` is unknown.

Este cambio de sintaxis sería una característica nueva y afectaría a los nuevos archivos de definición que se escriben si se usan como parámetro o tipo expuesto. Esta sintaxis podría combinarse con otros tipos más complejos.

type Foo = Exact<X> | Exact<Y>

type Bar = Exact<{ username: string }>

function insertIntoDb (user: Exact<User>) {}

Disculpe de antemano si se trata de un duplicado, parece que no pude encontrar las palabras clave adecuadas para encontrar duplicados de esta función.

Editar: esta publicación se actualizó para usar la propuesta de sintaxis preferida mencionada en https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -267272371, que abarca el uso de una sintaxis más simple con un tipo genérico para permitir el uso en expresiones.

Awaiting More Feedback Suggestion

Comentario más útil

Hablamos de esto durante bastante tiempo. Intentaré resumir la discusión.

Comprobación de exceso de propiedad

Los tipos exactos son solo una forma de detectar propiedades adicionales. La demanda de tipos exactos disminuyó mucho cuando implementamos inicialmente la verificación de exceso de propiedad (EPC). EPC fue probablemente el cambio más importante que hemos realizado, pero ha dado sus frutos; casi de inmediato obtuvimos errores cuando EPC no detectó un exceso de propiedad.

En la mayor parte de los casos en los que la gente quiere tipos exactos, preferiríamos solucionarlo haciendo que el EPC sea más inteligente. Un área clave aquí es cuando el tipo de destino es un tipo de unión; queremos tomar esto como una corrección de errores (EPC debería funcionar aquí, pero aún no está implementado).

Tipos totalmente opcionales

Relacionado con EPC está el problema de los tipos totalmente opcionales (que yo llamo tipos "débiles"). Lo más probable es que todos los tipos débiles quieran ser exactos. Deberíamos implementar la detección de tipos débiles (# 7485 / # 3842); el único bloqueador aquí son los tipos de intersección que requieren cierta complejidad adicional en la implementación.

¿De quién es el tipo exacto?

El primer problema importante que vemos con los tipos exactos es que realmente no está claro qué tipos deben marcarse como exactos.

En un extremo del espectro, tiene funciones que literalmente lanzarán una excepción (o harán cosas malas) si se les da un objeto con una clave propia fuera de algún dominio fijo. Estos son pocos y distantes entre sí (no puedo nombrar un ejemplo de memoria). En el medio, hay funciones que ignoran silenciosamente
propiedades desconocidas (casi todas). Y en el otro extremo tiene funciones que operan genéricamente sobre todas las propiedades (por ejemplo, Object.keys ).

Claramente, las funciones "arrojará si se le dan datos adicionales" deben marcarse como aceptando tipos exactos. Pero ¿qué pasa con el medio? Es probable que la gente no esté de acuerdo. Point2D / Point3D es un buen ejemplo; podría razonablemente decir que una función magnitude debería tener el tipo (p: exact Point2D) => number para evitar pasar un Point3D . Pero, ¿por qué no puedo pasar mi objeto { x: 3, y: 14, units: 'meters' } a esa función? Aquí es donde entra en juego EPC: desea detectar esa propiedad units "extra" en ubicaciones donde definitivamente se descarta, pero no bloquear realmente las llamadas que involucran aliasing.

Violaciones de supuestos / problemas de instanciación

Tenemos algunos principios básicos que los tipos exactos invalidarían. Por ejemplo, se supone que un tipo T & U siempre se puede asignar a T , pero esto falla si T es un tipo exacto. Esto es problemático porque puede tener alguna función genérica que use este principio T & U -> T , pero invoque la función con T instanciada con un tipo exacto. Por lo tanto, no hay forma de que podamos hacer este sonido (realmente no está bien cometer un error en la creación de instancias), no necesariamente un bloqueador, ¡pero es confuso que una función genérica sea más permisiva que una versión instanciada manualmente de sí misma!

También se asume que T siempre se puede asignar a T | U , pero no es obvio cómo aplicar esta regla si U es un tipo exacto. ¿Se puede asignar { s: "hello", n: 3 } a { s: string } | Exact<{ n: number }> ? "Sí" parece ser la respuesta incorrecta porque quien busque n y lo encuentre no estará feliz de ver s , pero "No" también parece incorrecto porque hemos violado el T -> T | U básico

Miscelánea

¿Cuál es el significado de function f<T extends Exact<{ n: number }>(p: T) ? :confundido:

A menudo se desean tipos exactos donde lo que realmente se desea es una unión "autodesarticulada". En otras palabras, es posible que tenga una API que pueda aceptar { type: "name", firstName: "bob", lastName: "bobson" } o { type: "age", years: 32 } pero no quiera aceptar { type: "age", years: 32, firstName: 'bob" } porque sucederá algo impredecible. El tipo "correcto" es posiblemente { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } pero bueno, es molesto escribirlo. Potencialmente, podríamos pensar en el azúcar para crear tipos como este.

Resumen: casos de uso necesarios

Nuestro diagnóstico esperanzador es que, fuera de las relativamente pocas API realmente cerradas, se trata de una solución de problema XY . Siempre que sea posible, deberíamos usar EPC para detectar propiedades "malas". Entonces, si tiene un problema y cree que los tipos exactos son la solución correcta, describa el problema original aquí para que podamos componer un catálogo de patrones y ver si hay otras soluciones que serían menos invasivas / confusas.

Todos 171 comentarios

Sugeriría que la sintaxis es discutible aquí. Dado que TypeScript ahora permite la tubería principal para el tipo de unión.

class B {}

type A = | number | 
B

Se compila ahora y equivale a type A = number | B , gracias a la inserción automática de punto y coma.

Creo que esto podría no esperar si se introduce el tipo exacto.

No estoy seguro si está relacionado, pero para su información https://github.com/Microsoft/TypeScript/issues/7481

Si se adoptó la sintaxis {| ... |} , podríamos construir sobre tipos mapeados para que pudiera escribir

type Exact<T> = {|
    [P in keyof T]: P[T]
|}

y luego podría escribir Exact<User> .

Esto es probablemente lo último que extraño de Flow, en comparación con TypeScript.

El ejemplo Object.assign es especialmente bueno. Entiendo por qué TypeScript se comporta como lo hace hoy, pero la mayoría de las veces prefiero tener el tipo exacto.

@HerringtonDarkholme Gracias. Mi problema inicial mencionó eso, pero lo omití al final, ya que alguien tendría una mejor sintaxis de todos modos, resulta que sí 😄

@DanielRosenwasser Eso parece mucho más razonable, ¡gracias!

@wallverb No lo creo, aunque también me gustaría que exista esa función 😄

¿Qué pasa si quiero expresar una unión de tipos, donde algunos de ellos son exactos y otros no? La sintaxis sugerida lo haría propenso a errores y difícil de leer, incluso si se presta especial atención al espaciado:

|Type1| | |Type2| | Type3 | |Type4| | Type5 | |Type6|

¿Puede decir rápidamente qué miembros del sindicato no son exactos?

¿Y sin el espaciado cuidadoso?

|Type1|||Type2||Type3||Type4||Type5||Type6|

(respuesta: Type3 , Type5 )

@rotemdan Vea la respuesta anterior, existe el tipo genérico Extact lugar, que es una propuesta más sólida que la mía. Creo que este es el enfoque preferido.

También existe la preocupación de cómo se vería en las sugerencias del editor, las ventanas emergentes de vista previa y los mensajes del compilador. Los alias de tipo actualmente simplemente "aplanan" a expresiones de tipo sin formato. El alias no se conserva, por lo que las expresiones incomprensibles seguirían apareciendo en el editor, a menos que se apliquen algunas medidas especiales para contrarrestar eso.

Me cuesta creer que esta sintaxis haya sido aceptada en un lenguaje de programación como Flow, que tiene uniones con la misma sintaxis que Typecript. A mí no me parece prudente introducir una sintaxis defectuosa que está fundamentalmente en conflicto con la sintaxis existente y luego esforzarme mucho por "cubrirla".

Una alternativa interesante (¿divertida?) Es usar un modificador como only . Creo que tenía un borrador de una propuesta para esto hace varios meses, pero nunca lo envié:

function test(a: only string, b: only User) {};

Esa fue la mejor sintaxis que pude encontrar en ese entonces.

_Editar_: ¿ just también podría funcionar?

function test(a: just string, b: just User) {};

_ (Editar: ahora que recuerdo que la sintaxis era originalmente para un modificador de tipos nominales, pero supongo que realmente no importa ... Los dos conceptos son lo suficientemente cercanos como para que estas palabras clave también funcionen aquí) _

Me preguntaba si quizás se podrían introducir ambas palabras clave para describir dos tipos de concordancia ligeramente diferentes:

  • just T (que significa: "exactamente T ") para una coincidencia estructural exacta, como se describe aquí.
  • only T (que significa: "únicamente T ") para la coincidencia nominal.

El emparejamiento nominal podría verse como una versión aún más "estricta" del emparejamiento estructural exacto. Significaría que no solo el tipo tiene que ser estructuralmente idéntico, el valor en sí debe estar asociado con el mismo identificador de tipo exacto especificado. Esto puede o no admitir alias de tipo, además de interfaces y clases.

Personalmente, no creo que la sutil diferencia crearía tanta confusión, aunque creo que depende del equipo de TypeScript decidir si el concepto de un modificador nominal como only parece apropiado. Solo sugiero esto como una opción.

_ (Editar: solo una nota sobre only cuando se usa con clases: hay una ambigüedad aquí sobre si permitiría subclases nominales cuando se hace referencia a una clase base; eso debe discutirse por separado, supongo. en menor grado, lo mismo podría considerarse para las interfaces, aunque actualmente no creo que sea tan útil) _

Esto parece una especie de tipos de resta disfrazados. Estos problemas pueden ser relevantes: https://github.com/Microsoft/TypeScript/issues/4183 https://github.com/Microsoft/TypeScript/issues/7993

@ethanresnick ¿Por qué crees eso?

Esto sería extremadamente útil en la base de código en la que estoy trabajando en este momento. Si esto ya fuera parte del lenguaje, no habría pasado hoy rastreando un error.

(Quizás otros errores, pero no este error en particular 😉)

No me gusta la sintaxis de tubería inspirada en Flow. Algo como exact palabra clave detrás de las interfaces sería más fácil de leer.

exact interface Foo {}

@ mohsen1 Estoy seguro de que la mayoría de la gente usaría el tipo genérico Exact en posiciones de expresión, por lo que no debería importar demasiado. Sin embargo, me preocuparía una propuesta como esa, ya que podría sobrecargar prematuramente la parte izquierda de la palabra clave de la interfaz que anteriormente se había reservado solo para exportaciones (siendo coherente con los valores de JavaScript, por ejemplo, export const foo = {} ). También indica que tal vez esa palabra clave también esté disponible para tipos (por ejemplo, exact type Foo = {} y ahora será export exact interface Foo {} ).

Con la sintaxis {| |} ¿cómo funcionaría extends ? ¿Será interface Bar extends Foo {| |} exacto si Foo no es exacto?

Creo que la palabra clave exact hace que sea fácil saber si una interfaz es exacta. También puede (¿debería?) Funcionar por type .

interface Foo {}
type Bar = exact Foo

Extremadamente útil para cosas que funcionan en bases de datos o llamadas de red a bases de datos o SDK como AWS SDK, que toman objetos con todas las propiedades opcionales, ya que los datos adicionales se ignoran silenciosamente y pueden generar errores difíciles o muy difíciles de encontrar: rose:

@ mohsen1 Esa pregunta parece irrelevante para la sintaxis, ya que la misma pregunta todavía existe usando el enfoque de palabras clave. Personalmente, no tengo una respuesta preferida y tendría que jugar con las expectativas existentes para responderla, pero mi reacción inicial es que no debería importar si Foo es exacta o no.

El uso de una palabra clave exact parece ambiguo: ¿está diciendo que se puede usar como exact interface Foo {} o type Foo = exact {} ? ¿Qué significa exact Foo | Bar ? Usar el enfoque genérico y trabajar con patrones existentes significa que no es necesario reinventar ni aprender. Es solo interface Foo {||} (esto es lo único nuevo aquí), luego type Foo = Exact<{}> y Exact<Foo> | Bar .

Hablamos de esto durante bastante tiempo. Intentaré resumir la discusión.

Comprobación de exceso de propiedad

Los tipos exactos son solo una forma de detectar propiedades adicionales. La demanda de tipos exactos disminuyó mucho cuando implementamos inicialmente la verificación de exceso de propiedad (EPC). EPC fue probablemente el cambio más importante que hemos realizado, pero ha dado sus frutos; casi de inmediato obtuvimos errores cuando EPC no detectó un exceso de propiedad.

En la mayor parte de los casos en los que la gente quiere tipos exactos, preferiríamos solucionarlo haciendo que el EPC sea más inteligente. Un área clave aquí es cuando el tipo de destino es un tipo de unión; queremos tomar esto como una corrección de errores (EPC debería funcionar aquí, pero aún no está implementado).

Tipos totalmente opcionales

Relacionado con EPC está el problema de los tipos totalmente opcionales (que yo llamo tipos "débiles"). Lo más probable es que todos los tipos débiles quieran ser exactos. Deberíamos implementar la detección de tipos débiles (# 7485 / # 3842); el único bloqueador aquí son los tipos de intersección que requieren cierta complejidad adicional en la implementación.

¿De quién es el tipo exacto?

El primer problema importante que vemos con los tipos exactos es que realmente no está claro qué tipos deben marcarse como exactos.

En un extremo del espectro, tiene funciones que literalmente lanzarán una excepción (o harán cosas malas) si se les da un objeto con una clave propia fuera de algún dominio fijo. Estos son pocos y distantes entre sí (no puedo nombrar un ejemplo de memoria). En el medio, hay funciones que ignoran silenciosamente
propiedades desconocidas (casi todas). Y en el otro extremo tiene funciones que operan genéricamente sobre todas las propiedades (por ejemplo, Object.keys ).

Claramente, las funciones "arrojará si se le dan datos adicionales" deben marcarse como aceptando tipos exactos. Pero ¿qué pasa con el medio? Es probable que la gente no esté de acuerdo. Point2D / Point3D es un buen ejemplo; podría razonablemente decir que una función magnitude debería tener el tipo (p: exact Point2D) => number para evitar pasar un Point3D . Pero, ¿por qué no puedo pasar mi objeto { x: 3, y: 14, units: 'meters' } a esa función? Aquí es donde entra en juego EPC: desea detectar esa propiedad units "extra" en ubicaciones donde definitivamente se descarta, pero no bloquear realmente las llamadas que involucran aliasing.

Violaciones de supuestos / problemas de instanciación

Tenemos algunos principios básicos que los tipos exactos invalidarían. Por ejemplo, se supone que un tipo T & U siempre se puede asignar a T , pero esto falla si T es un tipo exacto. Esto es problemático porque puede tener alguna función genérica que use este principio T & U -> T , pero invoque la función con T instanciada con un tipo exacto. Por lo tanto, no hay forma de que podamos hacer este sonido (realmente no está bien cometer un error en la creación de instancias), no necesariamente un bloqueador, ¡pero es confuso que una función genérica sea más permisiva que una versión instanciada manualmente de sí misma!

También se asume que T siempre se puede asignar a T | U , pero no es obvio cómo aplicar esta regla si U es un tipo exacto. ¿Se puede asignar { s: "hello", n: 3 } a { s: string } | Exact<{ n: number }> ? "Sí" parece ser la respuesta incorrecta porque quien busque n y lo encuentre no estará feliz de ver s , pero "No" también parece incorrecto porque hemos violado el T -> T | U básico

Miscelánea

¿Cuál es el significado de function f<T extends Exact<{ n: number }>(p: T) ? :confundido:

A menudo se desean tipos exactos donde lo que realmente se desea es una unión "autodesarticulada". En otras palabras, es posible que tenga una API que pueda aceptar { type: "name", firstName: "bob", lastName: "bobson" } o { type: "age", years: 32 } pero no quiera aceptar { type: "age", years: 32, firstName: 'bob" } porque sucederá algo impredecible. El tipo "correcto" es posiblemente { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } pero bueno, es molesto escribirlo. Potencialmente, podríamos pensar en el azúcar para crear tipos como este.

Resumen: casos de uso necesarios

Nuestro diagnóstico esperanzador es que, fuera de las relativamente pocas API realmente cerradas, se trata de una solución de problema XY . Siempre que sea posible, deberíamos usar EPC para detectar propiedades "malas". Entonces, si tiene un problema y cree que los tipos exactos son la solución correcta, describa el problema original aquí para que podamos componer un catálogo de patrones y ver si hay otras soluciones que serían menos invasivas / confusas.

El lugar principal en el que veo que la gente se sorprende al no tener un tipo de objeto exacto es en el comportamiento de Object.keys y for..in : siempre producen un tipo string lugar de 'a'|'b' para algo escrito { a: any, b: any } .

Como mencioné en https://github.com/Microsoft/TypeScript/issues/14094 y lo describiste en la sección Miscelánea, es molesto que {first: string, last: string, fullName: string} ajuste a {first: string; last: string} | {fullName: string} .

Por ejemplo, se supone que un tipo T & U siempre se puede asignar a T, pero esto falla si T es un tipo exacto

Si T es un tipo exacto, entonces presumiblemente T & U es never (o T === U ). ¿Derecha?

O U es un subconjunto no exacto de T

Mi caso de uso que me lleva a esta sugerencia son los reductores redux.

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

Como señaló en el resumen, mi problema no es directamente que necesite interfaces exactas, necesito que el operador de propagación funcione con precisión. Pero dado que el comportamiento del operador de propagación lo da JS, la única solución que me viene a la mente es definir el tipo de retorno o la interfaz para ser exactos.

¿Entiendo correctamente que asignar un valor de T a Exact<T> sería un error?

interface Dog {
    name: string;
    isGoodBoy: boolean;
}
let a: Dog = { name: 'Waldo', isGoodBoy: true };
let b: Exact<Dog> = a;

En este ejemplo, reducir Dog a Exact<Dog> no sería seguro, ¿verdad?
Considere este ejemplo:

interface PossiblyFlyingDog extends Dog {
    canFly: boolean;
}
let c: PossiblyFlyingDog = { ...a, canFly: true };
let d: Dog = c; // this is okay
let e: Exact<Dog> = d; // but this is not

@leonadler Sí, esa sería la idea. Solo puede asignar Exact<T> a Exact<T> . Mi caso de uso inmediato es que las funciones de validación manejarían los tipos Exact (por ejemplo, tomando cargas útiles de solicitud como any y generando Exact<T> válidos). Exact<T> , sin embargo, sería asignable a T .

@nerumo

Como señaló en el resumen, mi problema no es directamente que necesite interfaces exactas, necesito que el operador de propagación funcione con precisión. Pero dado que el comportamiento del operador de propagación lo da JS, la única solución que me viene a la mente es definir el tipo de retorno o la interfaz para ser exactos.

Me encontré con el mismo problema y descubrí esta solución que para mí es una solución bastante elegante :)

export type State = {
  readonly counter: number,
  readonly baseCurrency: string,
};

// BAD
export function badReducer(state: State = initialState, action: Action): State {
  if (action.type === INCREASE_COUNTER) {
    return {
      ...state,
      counterTypoError: state.counter + 1, // OK
    }; // it's a bug! but the compiler will not find it 
  }
}

// GOOD
export function goodReducer(state: State = initialState, action: Action): State {
  let partialState: Partial<State> | undefined;

  if (action.type === INCREASE_COUNTER) {
    partialState = {
      counterTypoError: state.counter + 1, // Error: Object literal may only specify known properties, and 'counterTypoError' does not exist in type 'Partial<State>'. 
    }; // now it's showing a typo error correctly 
  }
  if (action.type === CHANGE_BASE_CURRENCY) {
    partialState = { // Error: Types of property 'baseCurrency' are incompatible. Type 'number' is not assignable to type 'string'.
      baseCurrency: 5,
    }; // type errors also works fine 
  }

  return partialState != null ? { ...state, ...partialState } : state;
}

puedes encontrar más en esta sección de mi guía redux:

Tenga en cuenta que esto podría resolverse en el área de usuario utilizando mi propuesta de tipos de restricción (# 13257):

type Exact<T> = [
    case U in U extends T && T extends U: T,
];

Editar: sintaxis actualizada relativa a la propuesta

@piotrwitek gracias, el truco parcial funciona perfectamente y ya encontré un error en mi código base;) que vale la pena el pequeño código repetitivo. Pero aún estoy de acuerdo con @isiahmeadows en que un Exactsería aún mejor

@piotrwitek usando Parcial así _almost_ resolvió mi problema, pero aún permite que las propiedades se vuelvan indefinidas incluso si la interfaz de estado afirma que no lo son (supongo que estrictasNullChecks).

Terminé con algo un poco más complejo para preservar los tipos de interfaz:

export function updateWithPartial<S extends object>(current: S, update: Partial<S>): S {
    return Object.assign({}, current, update);
}

export function updateWith<S extends object, K extends keyof S>(current: S, update: {[key in K]: S[key]}): S {
    return Object.assign({}, current, update);
}

interface I {
    foo: string;
    bar: string;
}

const f: I = {foo: "a", bar: "b"}
updateWithPartial(f, {"foo": undefined}).foo.replace("a", "x"); // Compiles, but fails at runtime
updateWith(f, {foo: undefined}).foo.replace("a", "x"); // Does not compile
updateWith(f, {foo: "c"}).foo.replace("a", "x"); // Compiles and works

@asmundg eso es correcto, la solución aceptará undefined, pero desde mi punto de vista esto es aceptable, porque en mis soluciones estoy usando solo creadores de acciones con parámetros requeridos para la carga útil, y esto garantizará que ningún valor indefinido debería ser asignado a una propiedad que no acepta valores NULL.
Prácticamente estoy usando esta solución durante bastante tiempo en producción y este problema nunca sucedió, pero avíseme sus inquietudes.

export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';

export const actionCreators = {
  changeBaseCurrency: (payload: string) => ({
    type: CHANGE_BASE_CURRENCY as typeof CHANGE_BASE_CURRENCY, payload,
  }),
}

store.dispatch(actionCreators.changeBaseCurrency()); // Error: Supplied parameters do not match any signature of call target.
store.dispatch(actionCreators.changeBaseCurrency(undefined)); // Argument of type 'undefined' is not assignable to parameter of type 'string'.
store.dispatch(actionCreators.changeBaseCurrency('USD')); // OK => { type: "CHANGE_BASE_CURRENCY", payload: 'USD' }

DEMOSTRACIÓN - habilite estrictasNullChecks en las opciones

también puede hacer una carga útil que acepta valores NULL si es necesario, puede leer más en mi guía: https://github.com/piotrwitek/react-redux-typescript-guide#actions

Cuando los tipos de descanso se fusionan, esta función se puede convertir fácilmente en azúcar sintáctica sobre ellos.

Propuesta

La lógica de igualdad de tipos debe hacerse estricta: solo los tipos con las mismas propiedades o los tipos que tienen propiedades de descanso que se pueden instanciar de tal manera que sus tipos principales tienen las mismas propiedades se consideran coincidentes. Para preservar la compatibilidad con versiones anteriores, se agrega un tipo de descanso sintético a todos los tipos, a menos que ya exista uno. También se agrega una nueva bandera --strictTypes , que suprime la adición de parámetros de descanso sintéticos.

Acciones por debajo de --strictTypes :

type A = { x: number, y: string };
type B = { x: number, y: string, ...restB: <T>T };
type C = { x: number, y: string, z: boolean, ...restC: <T>T };

declare const a: A;
declare const b: B;
declare const c: C;

a = b; // Error, type B has extra property: "restB"
a = c; // Error, type C has extra properties: "z", "restC"
b = a; // OK, restB inferred as {}
b = c; // OK, restB inferred as { z: boolean, ...restC: <T>T }

c = a; // Error, type A is missing property: "z"
       // restC inferred as {}

c = b; // Error, type B is missing property: "z"
       // restC inferred as restB 

Si --strictTypes no se activa, una propiedad ...rest: <T>T se agrega automáticamente en el tipo A . De esta forma las líneas a = b; y a = c; ya no serán errores, como es el caso de la variable b en las dos líneas siguientes.

Unas palabras sobre violaciones de supuestos

se supone que un tipo T & U siempre se puede asignar a T, pero esto falla si T es un tipo exacto.

Sí, & permite una lógica falsa, pero también es el caso de string & number . Tanto string como number son tipos rígidos distintos que no se pueden intersecar, sin embargo, el sistema de tipos lo permite. Los tipos exactos también son rígidos, por lo que la inconsistencia sigue siendo constante. El problema radica en el operador & : no es sólido.

¿Es {s: "hello", n: 3} asignable a {s: string} | Exacto <{n: number}>.

Esto se puede traducir a:

type Test = { s: string, ...rest: <T>T } | { n: number }
const x: Test = { s: "hello", n: 3 }; // OK, s: string; rest inferred as { n: number }

Entonces la respuesta debería ser "sí". No es seguro unir tipos exactos con tipos no exactos, ya que los tipos no exactos subsumen todos los tipos exactos a menos que haya una propiedad discriminadora.

Re: la función f<T extends Exact<{ n: number }>(p: T) en el comentario anterior de @RyanCavanaugh , en una de mis bibliotecas me gustaría mucho implementar la siguiente función:

const checkType = <T>() => <U extends Exact<T>>(value: U) => value;

Es decir, una función que devuelve su parámetro con su mismo tipo exacto, pero al mismo tiempo también verifica si su tipo también es exactamente del mismo tipo que otro (T).

Aquí hay un ejemplo un poco artificial con tres de mis intentos fallidos para satisfacer ambos requisitos:

  1. Sin exceso de propiedades con respecto a CorrectObject
  2. Asignable a HasX sin especificar HasX como tipo de objeto
type AllowedFields = "x" | "y";
type CorrectObject = {[field in AllowedFields]?: number | string};
type HasX = { x: number };

function objectLiteralAssignment() {
  const o: CorrectObject = {
    x: 1,
    y: "y",
    // z: "z" // z is correctly prevented to be defined for o by Excess Properties rules
  };

  const oAsHasX: HasX = o; // error: Types of property 'x' are incompatible.
}

function objectMultipleAssignment() {
  const o = {
    x: 1,
    y: "y",
    z: "z",
  };
  const o2 = o as CorrectObject; // succeeds, but undesirable property z is allowed

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

function genericExtends() {
  const checkType = <T>() => <U extends T>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    z: "z", // undesirable property z is allowed
  }); // o is inferred to be { x: number; y: string; z: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

Aquí HasX es un tipo muy simplificado (el tipo real se asigna contra un tipo de esquema) que se define en una capa diferente a la constante en sí, por lo que no puedo hacer el tipo de o ser ( CorrectObject & HasX ).

Con Exact Types, la solución sería:

function exactTypes() {
  const checkType = <T>() => <U extends Exact<T>>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    // z: "z", // undesirable property z is *not* allowed
  }); // o is inferred to be { x: number; y: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

@ andy-ms

Si T es un tipo exacto, entonces presumiblemente T & U nunca lo es (o T === U). ¿Derecha?

Creo que T & U debería ser never solo si U es demostrablemente incompatible con T , por ejemplo, si T es Exact<{x: number | string}> y U es {[field: string]: number} , entonces T & U debe ser Exact<{x: number}>

Vea la primera respuesta a eso:

O U es un subconjunto no exacto de T

Yo diría, si U es asignable a T, entonces T & U === T . Pero si T y U son tipos exactos diferentes, entonces T & U === never .

En su ejemplo, ¿por qué es necesario tener una función checkType que no haga nada? ¿Por qué no tener const o: Exact<CorrectObject> = { ... } ?

Porque pierde la información de que x definitivamente existe (opcional en CorrectObject) y es número (número | cadena en CorrectObject). O tal vez he entendido mal lo que significa Exacto, pensé que solo evitaría propiedades extrañas, no que significaría de manera recurrente que todos los tipos deben ser exactamente iguales.

Una consideración más en la compatibilidad con los tipos exactos y contra el EPC actual es la refactorización: si la refactorización de Extracción de variables estuviera disponible, se perdería el EPC a menos que la variable extraída introdujera una anotación de tipo, que podría volverse muy detallada.

Para aclarar por qué apoyo los tipos exactos, no es para uniones discriminadas, sino para errores de ortografía y propiedades erróneamente extrañas en caso de que el tipo de restricción no se pueda especificar al mismo tiempo que el objeto literal.

@ andy-ms

Yo diría, si U es asignable a T, entonces T & U === T. Pero si T y U son tipos exactos diferentes, entonces T & U === nunca.

El operador de tipo & es un operador de intersección, el resultado es el subconjunto común de ambos lados, que tampoco es necesariamente igual. El ejemplo más simple que se me ocurre:

type T = Exact<{ x?: any, y: any }>;
type U = { x: any, y? any };

aquí T & U debe ser Exact<{ x: any, y: any }> , que es un subconjunto de T y U , pero ni T es un subconjunto de U (falta x) ni U es un subconjunto de T (falta y).

Esto debería funcionar independientemente de si T , U o T & U son tipos exactos.

@magnushiie Tiene un buen punto: los tipos exactos pueden limitar la asignabilidad de los tipos con un ancho mayor, pero aún permiten la asignabilidad de los tipos con una mayor profundidad. Entonces podría cruzar Exact<{ x: number | string }> con Exact<{ x: string | boolean }> para obtener Exact<{ x: string }> . Un problema es que esto no es realmente seguro para tipos si x no es de solo lectura; es posible que deseemos corregir ese error para tipos exactos, ya que significan optar por un comportamiento más estricto.

Los tipos exactos también se pueden usar para problemas de relaciones de argumentos de tipo con firmas de índice.

interface T {
    [index: string]: string;
}

interface S {
    a: string;
    b: string;
}

interface P extends S {
    c: number;
}

declare function f(t: T);
declare function f2(): P;
const s: S = f2();

f(s); // Error because an interface can have more fields that is not conforming to an index signature
f({ a: '', b: '' }); // No error because literals is exact by default

Aquí hay una forma engañosa de verificar el tipo exacto:

// type we'll be asserting as exact:
interface TextOptions {
  alignment: string;
  color?: string;
  padding?: number;
}

// when used as a return type:
function getDefaultOptions(): ExactReturn<typeof returnValue, TextOptions> {
  const returnValue = { colour: 'blue', alignment: 'right', padding: 1 };
  //             ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.
  return returnValue
}

// when used as a type:
function example(a: TextOptions) {}
const someInput = {padding: 2, colour: '', alignment: 'right'}
example(someInput as Exact<typeof someInput, TextOptions>)
  //          ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.

Desafortunadamente, actualmente no es posible hacer la aserción Exact como un parámetro de tipo, por lo que debe realizarse durante el tiempo de llamada (es decir, debe recordarlo).

Aquí están las utilidades de ayuda necesarias para que funcione (gracias a @ tycho01 por algunas de ellas):

type Exact<A, B extends Difference<A, B>> = AssertPassThrough<Difference<A, B>, A, B>
type ExactReturn<A, B extends Difference<A, B>> = B & Exact<A, B>

type AssertPassThrough<Actual, Passthrough, Expected extends Actual> = Passthrough;
type Difference<A, Without> = {
  [P in DiffUnion<keyof A, keyof Without>]: A[P];
}
type DiffUnion<T extends string, U extends string> =
  ({[P in T]: P } &
  { [P in U]: never } &
  { [k: string]: never })[T];

Ver: Patio de recreo .

¡Buena esa! @gcanti ( typelevel-ts ) y @pelotom ( type-zoo ) también podrían estar interesados. :)

Para cualquier persona interesada, encontré una forma sencilla de aplicar tipos exactos en los parámetros de función. Funciona en TS 2.7, al menos.

function myFn<T extends {[K in keyof U]: any}, U extends DesiredType>(arg: T & U): void;

EDITAR: Supongo que para que esto funcione debes especificar un objeto literal directamente en el argumento; esto no funciona si declaras una constante separada arriba y la pasas en su lugar. : / Pero una solución es usar la extensión de objetos en el sitio de la llamada, es decir, myFn({...arg}) .

EDITAR: lo siento, no leí que mencionaste solo TS 2.7. ¡Lo probaré allí!

@vaskevich Parece que no puedo hacer que funcione, es decir , no está detectando colour como una propiedad en exceso :

Cuando llegan los tipos condicionales (# 21316), puede hacer lo siguiente para requerir tipos exactos como parámetros de función, incluso para literales de objeto "no actualizados":

type Exactify<T, X extends T> = T & {
    [K in keyof X]: K extends keyof T ? X[K] : never
}

type Foo = {a?: string, b: number}

declare function requireExact<X extends Exactify<Foo, X>>(x: X): void;

const exact = {b: 1}; 
requireExact(exact); // okay

const inexact = {a: "hey", b: 3, c: 123}; 
requireExact(inexact);  // error
// Types of property 'c' are incompatible.
// Type 'number' is not assignable to type 'never'.

Por supuesto, si amplía el tipo, no funcionará, pero no creo que haya nada que realmente pueda hacer al respecto:

const inexact = {a: "hey", b: 3, c: 123} as Foo;
requireExact(inexact);  // okay

¿Pensamientos?

Parece que se está avanzando en los parámetros de la función. ¿Alguien ha encontrado una manera de imponer tipos exactos para un valor de retorno de función?

@jezzgoodwin no realmente. Consulte el n. ° 241, que es la causa principal de que los retornos de funciones no se verifiquen correctamente en busca de propiedades adicionales.

Un caso de uso más. Casi me encuentro con un error debido a la siguiente situación que no se informa como un error:

interface A {
    field: string;
}

interface B {
    field2: string;
    field3?: string;
}

type AorB = A | B;

const fixture: AorB[] = [
    {
        field: 'sfasdf',
        field3: 'asd' // ok?!
    },
];

( Patio de recreo )

La solución obvia para esto podría ser:

type AorB = Exact<A> | Exact<B>;

Vi una solución alternativa propuesta en # 16679 pero en mi caso, el tipo es AorBorC (puede crecer) y cada objeto tiene múltiples propiedades, por lo que es bastante difícil calcular manualmente el conjunto de fieldX?:never propiedades para cada tipo.

@michalstocki ¿No es el # 20863? Quiere que el control de exceso de propiedad en los sindicatos sea más estricto.

De todos modos, en ausencia de tipos exactos y una estricta verificación de exceso de propiedad en las uniones, puede hacer estas propiedades fieldX?:never programación en lugar de manualmente mediante el uso de tipos condicionales :

type AllKeys<U> = U extends any ? keyof U : never
type ExclusifyUnion<U> = [U] extends [infer V] ?
 V extends any ? 
 (V & {[P in Exclude<AllKeys<U>, keyof V>]?: never}) 
 : never : never

Y luego defina su sindicato como

type AorB = ExclusifyUnion<A | B>;

que se expande a

type AorB = (A & {
    field2?: undefined;
    field3?: undefined;
}) | (B & {
    field?: undefined;
})

automáticamente. También funciona para cualquier AorBorC .

Consulte también https://github.com/Microsoft/TypeScript/issues/14094#issuecomment -373780463 para obtener información exclusiva o implementación

@jcalz El tipo avanzado

const { ...fields } = o as AorB;

fields.field3.toUpperCase(); // it shouldn't be passed

Los campos de fields no son opcionales.

No creo que eso tenga mucho que ver con los tipos exactos, sino con lo que sucede cuando extiendes y luego desestructuras un objeto de tipo unión. Cualquier unión terminará aplanándose en un solo tipo similar a una intersección, ya que separa un objeto en propiedades individuales y luego las vuelve a unir; se perderá cualquier correlación o restricción entre los componentes de cada sindicato. No estoy seguro de cómo evitarlo ... si es un error, podría ser un problema aparte.

Obviamente, las cosas se comportarán mejor si escribe la protección antes de la desestructuración:

declare function isA(x: any): x is A;
declare function isB(x: any): x is B;

declare const o: AorB;
if (isA(o)) {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
} else {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
  if (fields.field3) {
    fields.field3.toUpperCase(); // okay
  }
}

No es que esto "arregle" el problema que ves, pero así es como esperaría que alguien actuara con un sindicato restringido.

Tal vez https://github.com/Microsoft/TypeScript/pull/24897 solucione el problema de propagación

Puede que llegue tarde a la fiesta, pero así es como puedes al menos asegurarte de que tus tipos coincidan exactamente:

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
const sureIsTrue: (fact: true) => void = () => {};
const sureIsFalse: (fact: false) => void = () => {};



declare const x: string;
declare const y: number;
declare const xAndYAreOfTheSameType: AreSame<typeof x, typeof y>;
sureIsFalse(xAndYAreOfTheSameType);  // <-- no problem, as expected
sureIsTrue(xAndYAreOfTheSameType);  // <-- problem, as expected

Ojalá pudiera hacer esto:

type Exact<A, B> = A extends B ? B extends A ? B : never : never;
declare function needExactA<X extends Exact<A, X>>(value: X): void;

¿La función descrita en este número ayudaría en un caso en el que una interfaz vacía / indexada coincida con tipos de objetos, como funciones o clases?

interface MyType
{
    [propName: string]: any;
}

function test(value: MyType) {}

test({});           // OK
test(1);            // Fails, OK!
test('');           // Fails, OK!
test(() => {});     // Does not fail, not OK!
test(console.log);  // Does not fail, not OK!
test(console);      // Does not fail, not OK!

La interfaz MyType solo define una firma de índice y se usa como el tipo del único parámetro de la función test . Parámetro pasado a la función de tipo:

  • Objeto literal {} , pasa. Comportamiento esperado.
  • La constante numérica 1 no pasa. Comportamiento esperado (_Argumento de tipo '1' no se puede asignar al parámetro de tipo 'MyType' ._)
  • El literal de cadena '' no pasa. Comportamiento esperado (_`Argumento de tipo '""' no se puede asignar al parámetro de tipo 'MyType' ._)
  • Declaración de función de flecha () => {} : Pasa. Comportamiento no esperado. Probablemente pasa porque las funciones son objetos?
  • Método de clase console.log Pases. Comportamiento no esperado. Similar a la función de flecha.
  • Pases de clase console . Comportamiento no esperado. ¿Probablemente porque las clases son objetos?

El punto es solo permitir variables que coincidan exactamente con la interfaz MyType por ser de ese tipo ya (y no convertidas implícitamente a él). TypeScript parece hacer muchas conversiones implícitas basadas en firmas, por lo que esto podría ser algo que no se puede admitir.

Disculpas si esto está fuera de tema. Hasta ahora, este problema es el más cercano al problema que expliqué anteriormente.

@ Janne252 Esta propuesta podría ayudarte indirectamente. Suponiendo que haya probado el Exact<{[key: string]: any}> obvio, aquí le explicamos por qué funcionaría:

  • Los literales de objeto pasan como se esperaba, como ya lo hacen con {[key: string]: any} .
  • Las constantes numéricas fallan como se esperaba, ya que los literales no se pueden asignar a {[key: string]: any} .
  • Los literales de cadena fallan como se esperaba, ya que no se pueden asignar a {[key: string]: any} .
  • Las funciones y los constructores de clases fallan debido a su firma call (no es una propiedad de cadena).
  • El objeto console pasa porque es solo eso, un objeto (no una clase). JS no hace ninguna separación entre los objetos y los diccionarios de clave / valor, y TS no es diferente aquí, aparte de la tipificación polimórfica de filas agregada. Además, TS no admite tipos dependientes del valor, y typeof es simplemente azúcar para agregar algunos parámetros adicionales y / o alias de tipos; no es tan mágico como parece.

@blakeembrey @michalstocki @ aleksey-bykov
Esta es mi forma de hacer tipos exactos:

type Exact<A extends object> = A & {__kind: keyof A};

type Foo = Exact<{foo: number}>;
type FooGoo = Exact<{foo: number, goo: number}>;

const takeFoo = (foo: Foo): Foo => foo;

const foo = {foo: 1} as Foo;
const fooGoo = {foo: 1, goo: 2} as FooGoo;

takeFoo(foo)
takeFoo(fooGoo) // error "[ts]
//Argument of type 'Exact<{ foo: number; goo: number; }>' is not assignable to parameter of type 'Exact<{ //foo: number; }>'.
//  Type 'Exact<{ foo: number; goo: number; }>' is not assignable to type '{ __kind: "foo"; }'.
//    Types of property '__kind' are incompatible.
//      Type '"foo" | "goo"' is not assignable to type '"foo"'.
//        Type '"goo"' is not assignable to type '"foo"'."

const takeFooGoo = (fooGoo: FooGoo): FooGoo => fooGoo;

takeFooGoo(fooGoo);
takeFooGoo(foo); // error "[ts]
// Argument of type 'Exact<{ foo: number; }>' is not assignable to parameter of type 'Exact<{ foo: number; // goo: number; }>'.
//  Type 'Exact<{ foo: number; }>' is not assignable to type '{ foo: number; goo: number; }'.
//    Property 'goo' is missing in type 'Exact<{ foo: number; }>'.

Funciona para parámetros de funciones, retornos e incluso para asignaciones.
const foo: Foo = fooGoo; // error
Sin gastos generales de tiempo de ejecución. El único problema es que cada vez que crea un nuevo objeto exacto, debe lanzarlo contra su tipo, pero en realidad no es un gran problema.

Creo que el ejemplo original tiene el comportamiento correcto: espero que interface s esté abierto. Por el contrario, espero que type s estén cerrados (y solo se cierran algunas veces ). Aquí hay un ejemplo de comportamiento sorprendente al escribir un tipo MappedOmit :
https://gist.github.com/donabrams/b849927f5a0160081db913e3d52cc7b3

El tipo MappedOmit en el ejemplo solo funciona según lo previsto para uniones discriminadas. Para las uniones no discriminadas, Typecript 3.2 pasa cuando se pasa cualquier intersección de los tipos en la unión.

Las soluciones anteriores que utilizan as TypeX o as any para lanzar tienen el efecto secundario de ocultar errores en la construcción. ¡Queremos que nuestro comprobador de tipos también nos ayude a detectar errores en la construcción! Además, hay varias cosas que podemos generar estáticamente a partir de tipos bien definidos. Las soluciones alternativas como las anteriores (o las soluciones provisionales de tipo nominal descritas aquí: https://gist.github.com/donabrams/74075e89d10db446005abe7b1e7d9481) impiden que esos generadores funcionen (aunque podemos filtrar _ campos iniciales, es una convención dolorosa eso es absolutamente evitable).

@ aleksey-bykov para su información, creo que su implementación es del 99% del camino, esto funcionó para mí:

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

const value1 = {};
const value2 = {a:1};

// works
const exactValue1: Exact<{}, typeof value1> = value1;
const exactValue1WithTypeof: Exact<typeof value1, typeof value1> = value1;

// cannot assign {a:number} to never
const exactValue1Fail: Exact<{}, typeof value2> = value2;
const exactValue1FailWithTypeof: Exact<typeof value1, typeof value2> = value2;

// cannot assign {} to never
const exactValue2Fail: Exact<{a: number}, typeof value1> = value1;
const exactValue2FailWithTypeof: Exact<typeof value2, typeof value1> = value1;

// works
const exactValue2: Exact<{a: number}, typeof value2> = value2;
const exactValue2WithTypeof: Exact<typeof value2, typeof value2> = value2;

wow, por favor deja las flores aquí, los regalos van en esa papelera

Una pequeña mejora que se puede hacer aquí:
Al usar la siguiente definición de Exact efectivamente se crea una resta de B de A como A & never tipos en todos los B Claves únicas de

type Omit<T, K> = Pick<T, Exclude<keyof T, keyof K>>;
type Exact<A, B = {}> = A & Record<keyof Omit<B, A>, never>;

Por último, quería poder hacer esto sin tener que agregar un uso de plantilla explícito del segundo argumento de plantilla B . Pude hacer que esto funcionara envolviendo con un método, no es ideal ya que afecta el tiempo de ejecución, pero es útil si realmente lo necesita:

function makeExactVerifyFn<T>() {
  return <C>(x: C & Exact<T, C>): C => x;
}

Uso de muestra:

interface Task {
  title: string;
  due?: Date;
}

const isOnlyTask = makeExactVerifyFn<Task>();

const validTask_1 = isOnlyTask({
    title: 'Get milk',
    due: new Date()  
});

const validTask_2 = isOnlyTask({
    title: 'Get milk'
});

const invalidTask_1 = isOnlyTask({
    title: 5 // [ts] Type 'number' is not assignable to type 'string'.
});

const invalidTask_2 = isOnlyTask({
    title: 'Get milk',
    procrastinate: true // [ts] Type 'true' is not assignable to type 'never'.
});

@danielnmsft Parece extraño dejar B en Exact<A, B> opcional en su ejemplo, especialmente si es necesario para una validación adecuada. De lo contrario, me parece bastante bueno. Sin embargo, se ve mejor llamado Equal .

@drabinowitz Su tipo Exact no representa realmente lo que se ha propuesto aquí y probablemente debería cambiarse el nombre a algo como AreExact . Quiero decir, no puedes hacer esto con tu tipo:

function takesExactFoo<T extends Exact<Foo>>(foo: T) {}

Sin embargo, su tipo es útil para implementar el tipo de parámetro exacto.

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

interface Foo {
    bar: any
}

function takesExactFoo <T>(foo: T & Exact<Foo, T>) {
                    //  ^ or `T extends Foo` to type-check `foo` inside the function
}

let foo = {bar: 123}
let foo2 = {bar: 123, baz: 123}

takesExactFoo(foo) // ok
takesExactFoo(foo2) // error

UPD1 Esto no creará una función de tiempo de ejecución +1 como en la solución de @danielnmsft y, por supuesto, es mucho más flexible.

UPD2 Me acabo de dar cuenta de que Daniel, de hecho, hizo básicamente el mismo tipo Exact que hizo @drabinowitz , pero uno más compacto y probablemente mejor. También me di cuenta de que hice lo mismo que había hecho Daniel. Pero dejaré mi comentario por si a alguien le resulta útil.

Esa definición de AreSame / Exact no parece funcionar para el tipo de sindicato.
Ejemplo: Exact<'a' | 'b', 'a' | 'b'> da como resultado never .
Aparentemente, esto se puede solucionar definiendo type AreSame<A, B> = A|B extends A&B ? true : false;

@nerumo definitivamente encontró esto para el mismo tipo de función reductora que mostró.

Un par de opciones adicionales de lo que tenías:

1 Puede configurar el tipo de retorno para que sea el mismo que el tipo de entrada con typeof . Más útil si es un tipo muy complicado. Para mí, cuando miro esto, es más explícitamente obvio que la intención es evitar propiedades adicionales.

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): typeof state {
   return {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   };
}

2 Para los reductores, en lugar de una variable temporal, asígnela de nuevo a sí misma antes de devolver:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {
   return (state = {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   });
}

3 Si realmente desea una variable temporal, no le dé un tipo explícito, use typeof state nuevamente

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: typeof state = {
       ...state,
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

3b Si su reductor no contiene ...state , puede usar Partial<typeof state> para el tipo:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: Partial<typeof state> = {
       name: 'Simon',
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

Siento que toda esta conversación (y acabo de leer todo el hilo) pasó por alto el meollo del problema para la mayoría de las personas y es que para evitar errores todo lo que queremos es una afirmación de tipo para evitar rechazar un tipo 'más amplio':

Esto es lo que la gente puede intentar primero, lo que no deshabilita 'fullName':

 return <State> {
       ...state,
       fullName: action.payload         // compiles ok :-(
   };

Esto se debe a que <Dog> cat está diciendo al compilador: sí, sé lo que estoy haciendo, ¡es un Dog ! No estás pidiendo permiso.

Entonces, lo que me sería más útil es una versión más estricta de <Dog> cat que evitaría propiedades extrañas:

 return <strict State> {
       ...state,
       fullName: action.payload     // compiles ok :-(
   };

Todo el asunto del tipo Exact<T> tiene muchas consecuencias (¡este es un hilo largo!). Me recuerda todo el debate sobre las 'excepciones marcadas' en el que es algo que crees que quieres, pero resulta que tiene muchos problemas (como de repente, cinco minutos después, querer un Unexact<T> ).

Por otro lado, <strict T> actuaría más como una barrera para evitar que los tipos 'imposibles' pasen 'a través'. Es esencialmente un filtro de tipo que pasa a través del tipo (como se hizo anteriormente con las funciones de tiempo de ejecución).

Sin embargo, sería fácil para los recién llegados asumir que impidió que se transmitieran "datos incorrectos" en los casos en que sería imposible que lo hicieran.

Entonces, si tuviera que hacer una sintaxis de propuesta, sería esta:

/// This syntax is ONLY permitted directly in front of an object declaration:
return <strict State> { ...state, foooooooooooooo: 'who' };

Volviendo al OP: en teoría [1] con tipos negados podrías escribir type Exact<T> = T & not Record<not keyof T, any> . Entonces, un Exact<{x: string}> prohibiría que se le asigne cualquier tipo con claves que no sean x . No estoy seguro de si eso es suficiente para satisfacer lo que todos piden aquí, pero parece encajar perfectamente en el OP.

[1] Digo en teoría porque eso también se basa en mejores firmas de índice

Tengo curiosidad por saber si tengo el problema que se describe aquí. Tengo un código como:

const Layers = {
  foo: 'foo'
  bar: 'bar'
  baz: 'baz'
}

type Groups = {
  [key in keyof Pick<Layers, 'foo' | 'bar'>]: number
}

const groups = {} as Groups

luego me permite establecer propiedades desconocidas, que es lo que no quiero:

groups.foo = 1
groups.bar = 2
groups.anything = 2 // NO ERROR :(

La configuración de anything todavía funciona, y el tipo de valor clave es any . Esperaba que fuera un error.

¿Es esto lo que se resolverá con este problema?

Resulta que debería haber estado haciendo

type Groups = {
  [key in keyof Pick<typeof Layers, 'foo' | 'bar'>]: number
}

Tenga en cuenta el uso agregado de typeof .

El complemento Atom atom-typescript estaba tratando de no fallar y finalmente se bloqueó. Cuando agregué typeof , las cosas volvieron a la normalidad y ya no se permitieron accesorios desconocidos, que es lo que esperaba.

En otras palabras, cuando no estaba usando typeof , atom-typescript estaba tratando de calcular el tipo en otros lugares del código donde estaba usando los objetos de tipo Groups , y me estaba permitiendo agregar accesorios desconocidos y mostrándome una sugerencia de tipo any para ellos.

Así que no creo que tenga el problema de este hilo.

Otra complicación podría ser cómo manejar las propiedades opcionales.

Si tiene un tipo que tiene propiedades opcionales, ¿qué significaría Exact<T> para esas propiedades?

export type PlaceOrderResponse = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharged?: number
};

¿ Exact<T> significa que se deben definir todas las propiedades opcionales? ¿Cómo lo especificarías? No 'indefinido' o 'nulo' porque eso tiene un efecto de tiempo de ejecución.

¿Requiere esto ahora una nueva forma de especificar un 'parámetro opcional requerido'?

Por ejemplo, ¿con qué tenemos que asignar amountCharged en el siguiente ejemplo de código para que satisfaga la 'exactitud' del tipo? No estamos siendo muy 'exactos' si no hacemos cumplir esta propiedad para que sea al menos 'reconocida' de alguna manera. ¿Es <never> ? No puede ser undefined o null .

const exactOrderResponse: Exact<PlaceOrderResponse> = 
{
   status: 'paymentFailed',
   orderNumber: '1001',
   amountCharged: ????      
};

Por lo tanto, puede estar pensando: sigue siendo opcional, y ahora es exactamente opcional, lo que se traduce en opcional . Y ciertamente, en tiempo de Exact<T> pegando un signo de interrogación.

¿Quizás es solo cuando se asigna un valor entre dos tipos que se debe realizar esta verificación? (Para hacer cumplir que ambos incluyen amountCharged?: number )

Introduzcamos un nuevo tipo aquí para los datos de entrada de un cuadro de diálogo:

export type OrderDialogBoxData = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharge?: number      // note the typo here!
};

Así que probemos esto:

// run the API call and then assign it to a dialog box.
const serverResponse: Exact<PlaceOrderResponse> = await placeOrder();
const dialogBoxData: Exact<OrderDialogBoxData> = serverResponse;    // SHOULD FAIL

Por supuesto, esperaría que esto fallara debido al error tipográfico, aunque esta propiedad es opcional en ambos.

Entonces volví a '¿Por qué queremos esto en primer lugar?' .
Creo que sería por estas razones (o un subconjunto dependiendo de la situación):

  • Evite errores tipográficos en los nombres de las propiedades
  • Si agregamos una propiedad a algún 'componente', queremos asegurarnos de que todo lo que lo use tenga que agregar esa propiedad también
  • Si eliminamos una propiedad de algún 'componente', debemos eliminarla en todas partes.
  • Asegúrese de no proporcionar propiedades adicionales innecesariamente (tal vez lo enviemos a una API y queremos mantener la carga útil reducida)

Si las 'propiedades opcionales exactas' no se manejan correctamente, ¡algunos de estos beneficios se rompen o se confunden enormemente!

También en el ejemplo anterior, acabamos de 'calzar' Exact para tratar de evitar errores tipográficos, ¡pero solo logramos hacer un gran lío! Y ahora es aún más frágil que nunca.

Creo que lo que a menudo necesito no es en realidad un tipo Exact<T> , es uno de estos dos:

NothingMoreThan<T> o
NothingLessThan<T>

Donde 'obligatorio opcional' es ahora una cosa. El primero no permite que el RHS de la asignación no defina nada adicional, y el segundo se asegura de que todo (incluidas las propiedades opcionales) se especifique en el RHS de una asignación.

NothingMoreThan sería útil para cargas útiles enviadas a través del cable, o JSON.stringify() y si tuviera un error porque tenía demasiadas propiedades en RHS, tendría que escribir código de tiempo de ejecución para seleccionar solo las propiedades necesarias. Y esa es la solución correcta, porque así es como funciona Javascript.

NothingLessThan es una especie de lo que ya tenemos en mecanografiado, para todas las asignaciones normales, excepto que debería considerar propiedades (optional?: number) opcionales.

No espero que estos nombres tengan ningún efecto, pero creo que el concepto es más claro y más detallado que Exact<T> ...

Entonces, quizás (si realmente lo necesitamos):

Exact<T> = NothingMoreThan<NothingLessThan<T>>;

o sería:

Exact<T> = NothingLessThan<NothingMoreThan<T>>;   // !!

Esta publicación es el resultado de un problema real que tengo hoy donde tengo un 'tipo de datos de cuadro de diálogo' que contiene algunas propiedades opcionales y quiero asegurarme de que lo que viene del servidor se le pueda asignar.

Nota final: NothingLessThan / NothingMoreThan tienen una 'sensación' similar a algunos de los comentarios anteriores donde el tipo A se extiende desde el tipo B, o B se extiende desde A. La limitación es que no abordaría propiedades opcionales (al menos no creo que puedan hacerlo hoy).

@simeyla Podrías salirte con la

  • "Nada menos que" son tipos normales. TS hace esto implícitamente, y cada tipo se trata como equivalente a for all T extends X: T .
  • "Nada más que" es básicamente lo contrario: es un for all T super X: T implícito

Una forma de elegir uno o ambos explícitamente sería suficiente. Como efecto secundario, puede especificar T super C Java como su T extends NothingMoreThan<C> . Así que estoy bastante convencido de que probablemente sea mejor que los tipos exactos estándar.

Sin embargo, creo que esto debería ser sintaxis. ¿Tal vez esto?

  • extends T - La unión de todos los tipos asignables a T, es decir, equivalente a T .
  • super T - La unión de todos los tipos T es asignable.
  • extends super T , super extends T - La unión de todos los tipos equivalente a T. Esto simplemente se sale de la cuadrícula, ya que solo el tipo puede ser asignable y asignarse a sí mismo.
  • type Exact<T> = extends super T - Sugar incorporado para el caso común anterior, para facilitar la legibilidad.
  • Dado que esto solo cambia la asignabilidad, aún podría tener cosas como uniones que sean exactas o súper tipos.

Esto también hace posible implementar # 14094 en el área de usuario simplemente haciendo que cada variante sea Exact<T> , como Exact<{a: number}> | Exact<{b: number}> .


Me pregunto si esto también hace posibles los tipos negados en la tierra de los usuarios. Creo que sí, pero primero tendría que hacer algunos tipos complicados de aritmética para confirmarlo, y no es exactamente algo obvio que probar.

Me pregunto si esto también hace posibles los tipos negados en la tierra de los usuarios, ya que (super T) | (extiende T) es equivalente a desconocido. Creo que lo es, pero primero necesitaría hacer algunos tipos complicados de aritmética para confirmarlo, y no es exactamente algo obvio que probar.

Para que (super T) | (extends T) === unknown mantenga la asignabilidad debería ser un pedido total.

@ jack-williams Buena captura y arreglada (eliminando el reclamo). Me preguntaba por qué las cosas no estaban funcionando inicialmente cuando estaba jugando un poco.

@ jack-williams

"Nada menos que" son tipos normales. TS hace esto implícitamente, y cada tipo se trata como equivalente

Si y no. Pero sobre todo sí ... ... ¡pero solo si estás en modo strict !

Así que tuve muchas situaciones en las que necesitaba que una propiedad fuera lógicamente "opcional", pero quería que el compilador me dijera si la había "olvidado" o si la había escrito mal.

Bueno, eso es exactamente lo que obtienes con lastName: string | undefined mientras que yo obtuve principalmente lastName?: string , y por supuesto sin el modo strict no se te advertirá de todas las discrepancias.

Siempre he sabido sobre el modo estricto, y por mi vida no puedo encontrar una buena razón por la que no lo encendí hasta ayer, pero ahora que sí (y todavía estoy revisando cientos de soluciones ) es mucho más fácil obtener el comportamiento que quería 'fuera de la caja'.

Había estado intentando todo tipo de cosas para conseguir lo que quería, incluido jugar con Required<A> extends Required<B> y tratar de eliminar las marcas de propiedad ? opcionales. Eso me envió por un agujero de conejo completamente diferente - (y todo esto fue antes de que activara el modo strict ).

El punto es que si está tratando de obtener algo cercano a los tipos 'exactos' hoy , debe comenzar habilitando el modo strict (o cualquier combinación de indicadores que proporcione las comprobaciones correctas). Y si tuviera que agregar middleName: string | undefined más tarde, entonces boom, de repente encontraría todos los lugares donde necesitaba 'considerarlo' :-)

PD. gracias por sus comentarios - fue muy útil. Me doy cuenta de que he visto MUCHO código que claramente no está usando el modo strict , y luego la gente se encuentra con las paredes como lo hice yo. Me pregunto qué se puede hacer para fomentar más su uso.

@simeyla ¡Creo que sus comentarios y agradecimientos deberían dirigirse a @isiahmeadows!

Pensé que escribiría mis experiencias con los tipos exactos después de implementar un prototipo básico. Mi opinión general es que el equipo acertó con su evaluación:

Nuestro diagnóstico esperanzador es que, fuera de las relativamente pocas API realmente cerradas, se trata de una solución de problema XY.

No creo que el costo de introducir otro tipo de objeto se reembolse detectando más errores o habilitando nuevas relaciones de tipos. En última instancia, los tipos exactos me dejaron _decir_ más, pero no me dejaron _hacer_ más.

Examinando algunos de los casos de usos potenciales de tipos exactos:

Escritura fuerte para keys y for ... in .

Tener tipos más precisos al enumerar claves parece atractivo, pero en la práctica nunca me encontré enumerando claves para cosas que eran conceptualmente exactas. Si conoce con precisión las claves, ¿por qué no abordarlas directamente?

Endurecimiento de propiedad opcional ampliación.

La regla de asignabilidad { ... } <: { ...; x?: T } no es sólida porque el tipo de la izquierda puede incluir una propiedad x incompatible que se eliminó con alias. Al asignar de un tipo exacto, esta regla se vuelve sólida. En la práctica, nunca utilizo esta regla; parece más adecuado para sistemas heredados que, para empezar, no tendrían tipos exactos.

Reaccionar y HOC

Había puesto mi última esperanza en los tipos exactos que mejoraban el paso de los accesorios y la simplificación de los tipos de propagación. La realidad es que los tipos exactos son la antítesis del polimorfismo acotado y fundamentalmente no composicionales.

Un genérico limitado le permite especificar los accesorios que le interesan y pasar el resto. Tan pronto como el límite se vuelve exacto, pierde por completo el subtipo de ancho y el genérico se vuelve significativamente menos útil. Otro problema es que una de las principales herramientas de composición en TypeScript es la intersección, pero los tipos de intersección son incompatibles con los tipos exactos. Cualquier tipo de intersección no trivial con un componente exacto será vacío: _ los tipos exactos no componen_. Para react y accesorios, probablemente desee tipos de fila y polimorfismo de fila, pero eso es para otro día.

Casi todos los errores interesantes que podrían resolverse con tipos exactos se resuelven mediante la comprobación de propiedades excesivas; El mayor problema es que el control de exceso de propiedad no funciona para los sindicatos sin una propiedad discriminatoria; resuelva esto y casi todos los problemas interesantes relevantes para los tipos exactos desaparecerán, en mi opinión.

@ jack-williams Estoy de acuerdo en que generalmente no es muy útil tener tipos exactos. El concepto de control de propiedad en exceso está cubierto por mi propuesta de operador super T , solo indirectamente porque la unión de todos los tipos a los que se puede asignar T no incluye los subtipos adecuados de T.

No estoy muy a favor de esto personalmente, aparte de tal vez un T super U *, ya que el único caso de uso cada vez que he encontrado para el exceso de comprobación de la propiedad se trata de servidores rotos, algo que por lo general se puede evitar por utilizando una función de contenedor para generar las solicitudes manualmente y eliminar el exceso de basura. Todos los demás problemas que he encontrado informados en este hilo hasta ahora podrían resolverse simplemente usando una unión discriminada simple.

* Esto sería básicamente T extends super U usando mi propuesta; los límites inferiores a veces son útiles para restringir tipos genéricos contravariantes, y las soluciones alternativas generalmente terminan introduciendo una gran cantidad de texto estándar adicional en mi experiencia.

@isiahmeadows Estoy de acuerdo en que los tipos delimitados más bajos pueden ser útiles, y si puede obtener tipos exactos de eso, entonces es una victoria para aquellos que quieran usarlos. Supongo que debería agregar una advertencia a mi publicación que es: estoy abordando principalmente el concepto de agregar un nuevo operador específicamente para tipos de objetos exactos.

@ jack-williams Creo que se perdió el matiz de que me refería principalmente a los tipos exactos y la parte relacionada de la comprobación de exceso de propiedad. La parte sobre los tipos delimitados inferiores fue una nota al pie por una razón: fue una digresión que solo está relacionada tangencialmente.

Me las arreglé para escribir una implementación para esto que funcionará para argumentos de función que requieren diversos grados de exactitud:

// Checks that B is a subset of A (no extra properties)
type Subset<A extends {}, B extends {}> = {
   [P in keyof B]: P extends keyof A ? (B[P] extends A[P] | undefined ? A[P] : never) : never;
}

// This can be used to implement partially strict typing e.g.:
// ('b?:' is where the behaviour differs with optional b)
type BaseOptions = { a: string, b: number }

// Checks there are no extra properties (Not More, Less fine)
const noMore = <T extends Subset<BaseOptions, T>>(options: T) => { }
noMore({ a: "hi", b: 4 })        //Fine
noMore({ a: 5, b: 4 })           //Error 
noMore({ a: "o", b: "hello" })   //Error
noMore({ a: "o" })               //Fine
noMore({ b: 4 })                 //Fine
noMore({ a: "o", b: 4, c: 5 })   //Error

// Checks there are not less properties (More fine, Not Less)
const noLess = <T extends Subset<T, BaseOptions>>(options: T) => { }
noLess({ a: "hi", b: 4 })        //Fine
noLess({ a: 5, b: 4 })           //Error
noLess({ a: "o", b: "hello" })   //Error
noLess({ a: "o" })               //Error  |b?: Fine
noLess({ b: 4 })                 //Error
noLess({ a: "o", b: 4, c: 5 })   //Fine

// We can use these together to get a fully strict type (Not More, Not Less)
type Strict<A extends {}, B extends {}> = Subset<A, B> & Subset<B, A>;
const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict({ a: "hi", b: 4 })        //Fine
strict({ a: 5, b: 4 })           //Error
strict({ a: "o", b: "hello" })   //Error
strict({ a: "o" })               //Error  |b?: Fine
strict({ b: 4 })                 //Error
strict({ a: "o", b: 4, c: 5 })   //Error

// Or a fully permissive type (More Fine, Less Fine)
type Permissive<A extends {}, B extends {}> = Subset<A, B> | Subset<B, A>;
const permissive = <T extends Permissive<BaseOptions, T>>(options: T) => { }
permissive({ a: "hi", b: 4 })        //Fine
permissive({ a: 5, b: 4 })           //Error
permissive({ a: "o", b: "hello" })   //Error
permissive({ a: "o" })               //Fine
permissive({ b: 4 })                 //Fine
permissive({ a: "o", b: 4, c: 5 })   //Fine


Tipo exacto para la asignación de variables que me di cuenta de que en realidad no hace nada ...

// This is a little unweildy, there's also a shortform that works in many cases:
type Exact<A extends {}> = Subset<A, A>
// The simpler Exact type works for variable typing
const options0: Exact<BaseOptions> = { a: "hi", b: 4 }        //Fine
const options1: Exact<BaseOptions> = { a: 5, b: 4 }           //Error
const options2: Exact<BaseOptions> = { a: "o", b: "hello" }   //Error
const options3: Exact<BaseOptions> = { a: "o" }               //Error |b?: Fine
const options4: Exact<BaseOptions> = { b: 4 }                 //Error
const options5: Exact<BaseOptions> = { a: "o", b: 4, c: 5 }   //Error

// It also works for function typing when using an inline value
const exact = (options: Exact<BaseOptions>) => { }
exact({ a: "hi", b: 4 })        //Fine
exact({ a: 5, b: 4 })           //Error
exact({ a: "o", b: "hello" })   //Error
exact({ a: "o" })               //Error  |b?: Fine
exact({ b: 4 })                 //Error
exact({ a: "o", b: 4, c: 5 })   //Error

// But not when using a variable as an argument even of the same type
const options6 = { a: "hi", b: 4 }
const options7 = { a: 5, b: 4 }
const options8 = { a: "o", b: "hello" }
const options9 = { a: "o" }
const options10 = { b: 4 }
const options11 = { a: "o", b: 4, c: 5 }
exact(options6)                 //Fine
exact(options7)                 //Error
exact(options8)                 //Error
exact(options9)                 //Error |b?: Fine
exact(options10)                //Error
exact(options11)                //Fine  -- Should not be Fine

// However using strict does work for that
// const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict(options6)                //Fine
strict(options7)                //Error
strict(options8)                //Error
strict(options9)                //Error |b?: Fine
strict(options10)               //Error
strict(options11)               //Error -- Is correctly Error

Ver

https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs

Siento que tengo un caso de uso para esto al empaquetar componentes de React, donde necesito "pasar" los accesorios: https://github.com/Microsoft/TypeScript/issues/29883. @ jack-williams ¿Alguna idea sobre esto?

@OliverJAsh Parece relevante, pero debo admitir que no conozco React tan bien como la mayoría. Supongo que sería útil analizar cómo los tipos exactos pueden ayudar con precisión aquí.

type MyComponentProps = { foo: 1 };
declare const MyComponent: ComponentType<MyComponentProps>;

type MyWrapperComponent = MyComponentProps & { myWrapperProp: 1 };
const MyWrapperComponent: ComponentType<MyWrapperComponent> = props => (
    <MyComponent
        // We're passing too many props here, but no error!
        {...props}
    />
);

Por favor corríjame en cualquier momento en que diga algo incorrecto.

Supongo que el comienzo sería especificar MyComponent para aceptar un tipo exacto.

declare const MyComponent: ComponentType<Exact<MyComponentProps>>;

En ese caso, obtendríamos un error, pero ¿cómo se corrige el error? Supongo aquí que los componentes de la envoltura no solo tienen el mismo tipo de accesorio hasta el final, y en algún momento realmente necesita extraer dinámicamente un subconjunto de accesorios. ¿Es esta una suposición razonable?

Si MyWrapperComponent props también es exacto, creo que sería suficiente hacer un enlace de desestructuración. En el caso genérico, esto requeriría un tipo Omit sobre un tipo exacto, y realmente no conozco la semántica allí. Supongo que podría funcionar como un tipo mapeado homomórfico y conservar la exactitud, pero creo que esto requeriría más reflexión.

Si MyWrapperComponent no es exacto, entonces se requerirá una verificación en tiempo de ejecución para demostrar la exactitud del nuevo tipo, que solo se puede hacer seleccionando explícitamente las propiedades que desea (que no se escalan como dice en su OP). No estoy seguro de cuánto gana en este caso.

Las cosas que no he cubierto porque no sé qué tan probables son es el caso genérico, donde props es un tipo genérico, y donde necesita combinar accesorios como { ...props1, ...props2 } . ¿Es esto común?

@Kotarski ¿Lo publicaste por casualidad en el registro de NPM?

@gitowiec

@Kotarski ¿Lo publicaste por casualidad en el registro de NPM?

https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs

Tengo este caso de uso:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

// I want this to error, because the 'c' should mean it prevents either AB or ABCD from being satisfied.
const foo: AB | ABCD = { a, b, c };

// I presume that I would need to do this:
const foo: Exact<AB> | Exact<ABCD> = { a, b, c };

@ ryami333 Eso no necesita tipos exactos; que solo necesita una solución al exceso de control de propiedad: # 13813.

@ ryami333 Si está dispuesto a usar un tipo adicional, tengo un tipo que hará lo que quiera, es decir, forzar una versión más estricta de las uniones:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD


type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

// Error now.
const foo: StrictUnion<AB | ABCD> = { a: "", b: "", c: "" };

@dragomirtitian Fascinante. Es curioso para mi porque

type KeyofV1<T extends object> = keyof T

produce un resultado diferente al

type KeyofV2<T> = T extends object ? keyof T : never

¿Podría alguien explicarme esto?

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

KeyofV1< AB | ABCD > // 'a' | 'b'
KeyofV2< AB | ABCD > // 'a' | 'b' | 'c' | 'e'

V1 obtiene las claves comunes del sindicato, V2 obtiene las claves de cada miembro del sindicato y los sindicatos obtienen el resultado.

@weswigham ¿Hay alguna razón por la que deberían devolver resultados diferentes?

¿Sí? Como dije, V1 obtiene las _ claves comunes_ para cada miembro del sindicato, porque el argumento de keyof termina siendo keyof (AB | ABCD) , que es solo "A" | "B" , mientras la versión dentro del condicional solo recibe un miembro del sindicato a la vez, gracias a la distribución condicional sobre su entrada, por lo que es esencialmente keyof AB | keyof ABCD .

@weswigham ¿Entonces el condicional lo evalúa más así, como a través de algún bucle implícito?

type Union =
    (AB extends object ? keyof AB : never) |
    (ABCD extends object ? keyof ABCD : never)

Cuando leo ese código, normalmente esperaría que el cheque (AB | ABCD) extends object funcione como una sola unidad, verificando que (AB | ABCD) se pueda asignar a object , y luego devuelva keyof (AB | ABCD) como una unidad, 'a' | 'b' . El mapeo implícito me parece realmente extraño.

@isiahmeadows Puede ver los tipos condicionales distributivos como un método para las uniones. Aplican el tipo condicional a cada miembro de la unión por turno y el resultado es la unión de cada resultado parcial.

Entonces UnionKeys<A | B> = UnionKeys<A> | UnionKeys<B> =(keyof A) | (keyof B)

Pero solo si el tipo condicional se distribuye, y solo se distribuye si el tipo probado es un parámetro de tipo desnudo. Entonces:

type A<T> = T extends object ? keyof T : never // distributive
type B<T> = [T] extends [object] ? keyof T : never // non distributive the type parameter is not naked
type B<T> = object extends T ? keyof T : never // non distributive the type parameter is not the tested type

Gracias chicos, creo que lo tengo. Lo reorganicé para mi comprensión; Creo que NegativeUncommonKeys es útil por sí solo. Aquí está en caso de que también sea útil para otra persona.

type UnionKeys<T> = T extends any ? keyof T : never;
type NegateUncommonKeys<T, TAll> = (
    Partial<
        Record<
            Exclude<
                UnionKeys<TAll>,
                keyof T
            >,
            never
        >
    >
) 

type StrictUnion<T, TAll = T> = T extends any 
  ? T & NegateUncommonKeys<T, TAll>
  : never;

También entiendo por qué T y TAll están allí. El "efecto de bucle", donde T es probado y desnudo, significa que cada elemento de la unión por T se aplica mientras que el TAll no probado contiene la unión original y completa de todos los elementos.

Este es el segmento del

@weswigham Sí ... excepto que creo que la sección se lee como si la hubiera escrito un ingeniero de compilación para otro ingeniero de compilación.

Los tipos condicionales en los que el tipo marcado es un parámetro de tipo desnudo se denominan tipos condicionales distributivos.

¿Qué son los parámetros de tipo desnudo? (y por qué no se ponen ropa 😄)

es decir, T se refiere a los constituyentes individuales después de que el tipo condicional se distribuye sobre el tipo de unión)

Ayer mismo tuve una discusión sobre lo que significa esta oración en particular y por qué había un énfasis en la palabra 'después'.

Creo que la documentación está escrita asumiendo conocimientos y terminología previos que los usuarios no siempre pueden tener.

La sección del manual tiene sentido para mí y lo explica mucho mejor, pero todavía soy escéptico sobre la elección del diseño allí. Simplemente no tiene sentido lógicamente para mí cómo ese comportamiento se seguiría naturalmente desde una perspectiva de teoría de conjuntos y de teoría de tipos. Simplemente parece un poco demasiado hack.

se siguen naturalmente de una perspectiva de la teoría de conjuntos y de la teoría de tipos

Tome cada elemento de un conjunto y divídalo de acuerdo con un predicado.

¡Esa es una operación distributiva!

Tome cada elemento de un conjunto y divídalo de acuerdo con un predicado.

Aunque eso solo tiene sentido cuando se habla de conjuntos de conjuntos (es decir, un tipo de unión) que comienza a sonar mucho más como teoría de categorías.

@RyanCavanaugh De acuerdo, déjame aclarar: intuitivamente leo T extends U ? F<T> : G<T> como T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T) , con la comparación hecha no por partes, sino como un paso completo. Eso es claramente diferente de "la unión de para todos {if t ∈ U then F({t}) else G({t}) | t ∈ T} , que es lo que actualmente es la semántica.

(Disculpe si mi sintaxis está un poco fuera de lugar, mi conocimiento de la teoría de tipos es totalmente autodidacta, así que sé que no conozco todos los formalismos sintácticos).

Qué operación es más intuitiva es objeto de un debate infinito, pero con las reglas actuales es fácil hacer que un tipo distributivo no sea distributivo con [T] extends [C] . Si el valor predeterminado no fuera distributivo, necesitaría un nuevo encantamiento en un nivel diferente para causar distributividad. Esa es también una pregunta separada de cuál comportamiento se prefiere con mayor frecuencia; IME Casi nunca quiero un tipo que no distribuya.

Sí, no hay una base teórica sólida para la distribución porque es una operación sintáctica.

La realidad es que es muy útil y tratar de codificarlo de otra manera sería doloroso.

Tal como está, seguiré adelante y me desviaré antes de alejar demasiado la conversación del tema.

Ya hay tantos problemas sobre la distrubutividad, ¿por qué no nos enfrentamos a que se requiere una nueva sintaxis?

30572

Aquí hay un problema de ejemplo:

Quiero especificar que el punto final / servicio de la API de mi usuario NO debe devolver ninguna propiedad adicional (como por ejemplo, contraseña) que no sean las especificadas en la interfaz de servicio. Si devuelvo accidentalmente un objeto con propiedades adicionales, quiero un error de tiempo de compilación, independientemente de si el objeto de resultado ha sido producido por un objeto literal o no.

Una verificación del tiempo de ejecución de cada objeto devuelto puede ser costosa, especialmente para las matrices.

El exceso de control de propiedad no ayuda en este caso. Honestamente, creo que es una solución inestable de un solo truco. En teoría, debería haber proporcionado un tipo de experiencia de "simplemente funciona"; en la práctica, también es una fuente de confusión. Los tipos de objetos exactos deberían haberse implementado en su lugar, habrían cubierto ambos casos de uso muy bien.

@babakness Tu tipo NoExcessiveProps es un no-op. Creo que se refieren a algo como esto:

interface API {
    username: () => { username: string }
}

const api: API = {
    username: (): { username: string } => {
        return { username: 'foobar', password: 'secret'} // error, ok
    }
}

const api2: API = {
    username: (): { username: string } => {
        const id: <X>(x: X) => X = x => x;
        const value = id({ username: 'foobar', password: 'secret' });
        return value  // no error, bad?
    }
}

Como escritor del tipo de API, desea hacer cumplir que username solo devuelve el nombre de usuario, pero cualquier implementador puede evitarlo porque los tipos de objetos no tienen restricciones de ancho. Eso solo se puede aplicar en la inicialización de un literal, lo que el implementador puede o no puede hacer. Sin embargo, desalentaría en gran medida a cualquiera que intente utilizar tipos exactos como seguridad basada en el lenguaje.

@espion

El exceso de control de propiedad no ayuda en este caso. Honestamente, creo que es una solución inestable de un solo truco. En teoría, deberían haber proporcionado una experiencia de tipo "simplemente funciona"

EPC es una opción de diseño razonablemente sensible y liviana que las cubiertas son un gran conjunto de problemas. La realidad es que los tipos exactos no "simplemente funcionan". Implementar de una manera sólida que admita la extensibilidad requiere un sistema de tipos completamente diferente.

@ jack-williams Por supuesto, también habría otras formas de verificar el presente (comprobaciones en tiempo de ejecución donde el rendimiento no es un problema, pruebas, etc.) pero una en tiempo de compilación adicional es invaluable para una retroalimentación rápida.

Además, no quise decir que los tipos exactos "simplemente funcionan". Quise decir que EPC estaba destinado a "simplemente funcionar", pero en la práctica es limitado, confuso e inseguro. Principalmente porque si intentas usarlo "deliberadamente", generalmente terminas disparándote en el pie.

editar: Sí, edité para reemplazar "ellos" con "eso" cuando me di cuenta de que era confuso.

@espion

Además, no quise decir que los tipos exactos "simplemente funcionan". Quise decir que EPC estaba destinado a "simplemente funcionar", pero en la práctica es limitado, confuso e inseguro. Principalmente porque si intentas usarlo "deliberadamente", generalmente terminas disparándote en el pie.

Mi error. Lea el comentario original como

En teoría, deberían haber proporcionado un tipo de experiencia "simplemente funciona" [que habría sido tipos exactos en lugar de EPC]

comentario en [] ser mi lectura.

La declaración revisada:

En teoría , debería haber proporcionado un tipo de experiencia de "simplemente funciona"

es mucho más claro. ¡Perdón por mi mala interpretación!

type NoExcessiveProps<O> = {
  [K in keyof O]: K extends keyof O ? O[K] : never 
}

// no error
const getUser1 = (): {username: string} => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
} 

// Compile-time error, OK
const foo: NoExcessiveProps<{username: string}>  = {username: 'a', password: 'b' }

// No error? 🤔
const getUser2 = (): NoExcessiveProps<{username: string}> => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
}


El resultado de getUser2 es sorprendente, se siente inconsistente y debería producir un error en tiempo de compilación. ¿Cuál es la idea de por qué no lo hace?

@babakness Tu NoExcessiveProps solo se evalúa como T (bueno, un tipo con las mismas claves que T ). En [K in keyof O]: K extends keyof O ? O[K] : never , K siempre será una clave de O ya que está mapeando más de keyof O . Sus errores de ejemplo de const porque activa EPC tal como lo habría hecho si lo hubiera escrito como {username: string} .

Si no le importa llamar a una función adicional, podemos capturar el tipo real del objeto pasado y realizar una forma personalizada de comprobaciones de exceso de propiedad. (Me doy cuenta de que el objetivo es detectar automáticamente este tipo de error, por lo que esto podría tener un valor limitado):

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked(foo) //ok
}

@dragomirtitian Ah ... cierto ... ¡buen punto! Entonces estoy tratando de entender su función checked . Estoy particularmente desconcertado

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    const bar = checked(foo) // error
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    const bar = checked(foo) // error!?
    return checked(foo) //ok
}

La asignación bar en getUser3 falla. El error parece estar en foo
image

Detalles del error

image

El tipo de bar aquí es {} , que parece ser porque en checked

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

E no se asigna en ningún lugar. Sin embargo, si reemplazamos typeof E con typeof {} , no funciona.

¿Cuál es el tipo de E? ¿Está sucediendo algún tipo de cosa que tenga en cuenta el contexto?

@babakness Si no hay otro lugar para inferir un parámetro de tipo, mecanografiado lo inferirá del tipo de retorno. Entonces, cuando asignamos el resultado de checked al retorno de getUser* , E será el tipo de retorno de la función, y T será el tipo real del valor que desea devolver. Si no hay un lugar para inferir E , el valor predeterminado será {} y, por lo tanto, siempre obtendrá un error.

La razón por la que lo hice así fue para evitar cualquier parámetro de tipo explícito, podría crear una versión más explícita:

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked<{ username: string }>()(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked<{ username: string }>()(foo) //ok
}

Nota: El enfoque de la función curry es necesario ya que aún no tenemos inferencia de argumentos parciales (https://github.com/Microsoft/TypeScript/pull/26349) por lo que no podemos especificar algún parámetro de tipo y hacer que otros se infieran en el misma llamada. Para evitar esto, especificamos E en la primera llamada y permitimos que se infiera T en la segunda llamada. También puede almacenar en caché la función cache para un tipo específico y usar la versión en caché

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}
const checkUser = checked<{ username: string }>()

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checkUser(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checkUser(foo) //ok
}

FWIW esta es una regla WIP / sketch tslint que resuelve el problema específico de no devolver accidentalmente propiedades adicionales de métodos "expuestos".

https://gist.github.com/spion/b89d1d2958f3d3142b2fe64fea5e4c32

Para el caso de uso de propagación, consulte https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -300382189: ¿podría un linter detectar un patrón como este y advertir que no es seguro para los tipos?

Copiando el ejemplo de código del comentario mencionado anteriormente:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

cc @JamesHenry / @ armano2

Me gustaría mucho que eso sucediera. Usamos definiciones de TypeScript generadas para los puntos finales de GraphQL y es un problema que TypeScript no genera un error cuando paso un objeto con más campos de los necesarios a una consulta porque GraphQL no ejecutará dicha consulta en tiempo de ejecución.

¿Cuánto de esto se aborda ahora con la actualización 3.5.1 con una mejor verificación de propiedades adicionales durante la asignación? tenemos un montón de áreas problemáticas conocidas marcadas como errores de la forma en que queríamos que fueran después de actualizar a 3.5.1

Si tiene un problema y cree que los tipos exactos son la solución correcta, describa el problema original aquí.

https://github.com/microsoft/TypeScript/issues/12936#issuecomment -284590083

Aquí hay uno que involucra a las referencias de React: https://github.com/microsoft/TypeScript/issues/31798

/ cc @RyanCavanaugh

Un caso de uso para mí es

export const mapValues =
  <T extends Exact<T>, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V) => {
    type TResult = Exact<{ [K in keyof T]: V }>;
    const result: Partial<TResult> = { };
    for (const [key, value] of Object.entries(object)) {
      result[key] = mapper(value, key);
    }
    return result as TResult;
  };

Esto no es correcto si no usamos tipos exactos, ya que si object tiene propiedades adicionales, no es seguro llamar a mapper en esas claves y valores adicionales.

La verdadera motivación aquí es que quiero tener los valores de una enumeración en algún lugar que pueda reutilizar en el código:

const choices = { choice0: true, choice1: true, choice2: true };
const callbacksForChoices = mapValues(choices, (_, choice) => () => this.props.callback(choice));

donde this.props.callback tiene el tipo (keyof typeof choices) => void .

Entonces, realmente se trata de que el sistema de tipos sea capaz de representar el hecho de que tengo una lista de claves en la tierra de código que coincide exactamente con un conjunto (por ejemplo, una unión) de claves en la tierra de tipo, de modo que podamos escribir funciones que operen en este lista de claves y hacer afirmaciones de tipo válidas sobre el resultado. No podemos usar un objeto ( choices en mi ejemplo anterior) porque hasta donde sabe el sistema de tipos, el objeto de código-tierra podría tener propiedades adicionales más allá de cualquier tipo de objeto que se use. No podemos usar una matriz ( ['choice0', 'choice1', 'choice2'] as const , porque hasta donde sabe el sistema de tipos, es posible que la matriz no contenga todas las claves permitidas por el tipo de matriz.

¿Quizás exact no debería ser un tipo, sino solo un modificador en las entradas y / o salidas de la función? Algo como el modificador de varianza del flujo ( + / - )

Quiero agregar a lo que acaba de decir @phaux . El uso real que tengo de Exact es que el compilador garantice la forma de las funciones. Cuando tengo un marco, es posible que desee cualquiera de estos: (T, S): AtMost<T> , (T, S): AtLeast<T> o (T, S): Exact<T> donde el compilador puede verificar que las funciones que define un usuario encajarán exactamente.

Algunos ejemplos útiles:
AtMost es útil para la configuración (por lo que no ignoramos params / errores tipográficos adicionales y fallamos antes).
AtLeast es ideal para cosas como componentes de reacción y middleware, donde un usuario puede introducir cualquier elemento adicional que desee en un objeto.
Exact es útil para la serialización / deserialización (podemos garantizar que no descartamos datos y estos son isomorfos).

¿Ayudaría esto a evitar que esto suceda?

interface IDate {
  year: number;
  month: number;
  day: number;
}

type TBasicField = string | number | boolean | IDate;

 // how to make this generic stricter?
function doThingWithOnlyCorrectValues<T extends TBasicField>(basic: T): void {
  // ... do things with basic field of only the exactly correct structures
}

const notADate = {
  year: 2019,
  month: 8,
  day: 30,
  name: "James",
};

doThingWithOnlyCorrectValues(notADate); // <- this should not work! I want stricter type checking

Realmente necesitamos una forma en TS para decir T extends exactly { something: boolean; } ? xxx : yyy .

O de lo contrario, algo como:

const notExact = {
  something: true,
  name: "fred",
};

Todavía devolverá xxx allí.

¿Quizás se pueda usar la palabra clave const ? por ejemplo, T extends const { something: boolean }

@pleerock puede ser un poco ambiguo, ya que en JavaScript / TypeScript podemos definir una variable como const pero aún así agregar / eliminar propiedades del objeto. Creo que la palabra clave exact es bastante precisa.

No estoy seguro de si está exactamente relacionado, pero esperaría al menos dos errores en este caso:
patio de recreo
Screen Shot 2019-08-08 at 10 15 34

@mityok Creo que está relacionado. Supongo que le gustaría hacer algo como:

class Animal {
  makeSound(): exact Foo {
     return { a: 5 };
  }
}

Si exact hizo que el tipo fuera más estricto, entonces no debería ser ampliable con una propiedad adicional, como lo hizo en Dog .

aprovechando el const ( as const ) y usando interfaces y tipos anteriores, como

const type WillAcceptThisOnly = number

function f(accept: WillAcceptThisOnly) {
}

f(1) // error
f(1 as WillAcceptThisOnly) // ok, explicit typecast

const n: WillAcceptThisOnly = 1
f(n) // ok

sería muy detallado tener que asignar a variables constantes, pero evitaría muchos casos extremos cuando pasa un tipo de alias que no es exactamente lo que esperaba

Se me ocurrió una solución pura de TypeScript para el problema Exact<T> que, creo, se comporta exactamente como lo que se solicitó en la publicación principal:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

La razón por la que ExactInner debe incluirse en Exact se debe a que la corrección # 32824 aún no se ha publicado (pero ya se ha fusionado en ! 32924 ).

Solo es posible asignar un valor a la variable o argumento de función de tipo Exact<T> , si la expresión de la derecha también es Exact<T> , donde T es exactamente de tipo idéntico en ambas partes de asignación.

No he logrado la promoción automática de valores en tipos exactos, así que para eso es la función exact() helper. Se puede promover cualquier valor para que sea del tipo exacto, pero la asignación solo tendrá éxito si TypeScript puede demostrar que los tipos subyacentes de ambas partes de expresión no solo son extensibles, sino exactamente iguales.

Funciona explotando el hecho de que TypeScript usa extend verificación de relación para determinar si el tipo de la mano derecha se puede asignar al tipo de la mano izquierda; solo puede hacerlo si el tipo de la mano derecha (fuente) _extends_ el tipo de la mano izquierda (destino) .

Citando checker.ts ,

// ¿Dos tipos condicionales 'T1 extiende U1? X1: ¿Y1 'y' T2 extiende U2? X2: Y2 'están relacionados si
// uno de T1 y T2 está relacionado con el otro, U1 y U2 son tipos idénticos, X1 está relacionado con X2,
// y Y1 está relacionado con Y2.

ExactInner<T> genérico utiliza el enfoque descrito, sustituyendo U1 y U2 con tipos subyacentes que requieren controles de exactitud. Exact<T> agrega una intersección con el tipo subyacente simple, lo que permite que TypeScript relaje el tipo exacto cuando su variable de destino o argumento de función no es un tipo exacto.

Desde la perspectiva del programador, Exact<T> comporta como si pusiera una bandera exact en T , sin inspeccionar T o cambiarlo, y sin crear un tipo independiente.

Aquí están el enlace del patio de recreo y el enlace esencial .

Una posible mejora futura sería permitir la promoción automática de tipos no exactos en tipos exactos, eliminando por completo la necesidad de la función exact() .

¡Trabajo increíble @toriningen!

Si alguien puede encontrar una manera de hacer que esto funcione sin tener que ajustar su valor en una llamada a exact , sería perfecto.

No estoy seguro de si este es el problema correcto, pero aquí hay un ejemplo de algo en lo que me gustaría trabajar.

https://www.typescriptlang.org/play/#code/KYOwrgtgBAyg9gJwC4BECWDgGMlriKAbwCgooBBAZyygF4oByAQ2oYBpSoVhq7GATHlgbEAvsWIAzMCBx4CTfvwDyCQQgBCATwAU -DNlz4AXFABE5GAGEzUAD7mUAUWtmAlEQnjiilWuCauvDI6Jhy + AB0VFgRSHAAqgAOiQFWLMA6bm4A3EA

enum SortDirection {
  Asc = 'asc',
  Desc = 'desc'
}
function addOrderBy(direction: "ASC" | "DESC") {}
addOrderBy(SortDirection.Asc.toUpperCase());

@lookfirst Eso es diferente. Esto solicita una función para tipos que no admiten propiedades adicionales, como algunos tipos exact {foo: number} donde {foo: 1, bar: 2} no se le pueden asignar. Eso es solo pedir transformaciones de texto para aplicar a los valores de enumeración, que probablemente no existan.

No estoy seguro de si este es el problema correcto, pero [...]

En mi experiencia como mantenedor en otro lugar, si tiene dudas y no puede encontrar ningún problema existente claro, presente un nuevo error y, en el peor de los casos, se cierra como un engaño que no encontró. Este es prácticamente el caso en la mayoría de los principales proyectos JS de código abierto. (La mayoría de nosotros, los mantenedores más importantes de la comunidad JS, somos en realidad personas decentes, solo personas que pueden atascarse realmente con los informes de errores y demás, por lo que es difícil no ser muy conciso a veces).

@isiahmeadows Gracias por la respuesta. No presenté un problema nuevo porque primero estaba buscando problemas duplicados, que es lo correcto. Estaba tratando de evitar atascar a la gente porque no estaba seguro de si este era el tema correcto o no, o incluso de cómo categorizar lo que estaba hablando.

EDITADO: Verifique la solución de @aigoncharov a continuación , porque creo que es aún más rápido.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

No sé si esto se puede mejorar más.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Sin comentarios

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

No sé si esto se puede mejorar más.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Sin comentarios

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

¡Amo esa idea!

Otro truco que podría funcionar es verificar la asignabilidad en ambas direcciones.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: string
}
type B = {
  prop1: string
  prop2: string
}
type C = {
  prop1: string
}

type ShouldBeNever = Exact<A, B>
type ShouldBeA = Exact<A, C>

http://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + + KAXinSgjmAgDsATAZ1wCgooB XMi6 + + k5lt3vygAuKFQgA3CACc o8VNmNQkKAEEiUAN58w0gPZgAjKLrBpASyoBzRgF9l4aACFNOlnsMmoZyzd0GYABMpuZWtg4q0ADCbgFeoX4RjI6qAMoAFvoArgA2NM4QAHKSMprwyGhq2M54qdCZOfmFGsQVKKjVUNF1QkA

Otra zona de juegos de @iamandrewluca https://www.typescriptlang.org/play/?ssl=7&ssc=6&pln=7&pc=17#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + + KAXinSgjmAgDsATAZ1wCgooB XMi6 + k5lt3vygAuKFQgA3CACc + + o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEoglyyc4DyqAogrACYK0Q1yRt02-RdlfuAist8-NoB6ZagAEnhIFBhpaVNZQuAhxZagA

Un matiz aquí es si Exact<{ prop1: 'a' }> debe asignarse a Exact<{ prop1: string }> . En mis casos de uso, debería.

@jeremybparagon, su caso está cubierto. A continuación se muestran algunos casos más.

type InexactType = {
    foo: 'foo'
}

const obj = {
    // here foo is infered as `string`
    // and will error because `string` is not assignable to `"foo"`
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj) // $ExpectError
type InexactType = {
    foo: 'foo'
}

const obj = {
    // here we cast to `"foo"` type
    // and will not error
    foo: 'foo' as 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)
type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

Creo que cualquiera que use este truco (y no estoy diciendo que no haya usos válidos para él) debería ser muy consciente de que es muy, muy fácil obtener más accesorios en el tipo "exacto". Dado que InexactType se puede asignar a Exact<T, InexactType> si tiene algo como esto, se sale de la exactitud sin darse cuenta:

function test1<T>(t: Exact<T, InexactType>) {}

function test2(t: InexactType) {
  test1(t); // inexactType assigned to exact type
}
test2(obj) // but 

Enlace de patio de recreo

Esta es la razón (al menos una de ellas) por la que TS no tiene tipos exactos, ya que requeriría una bifurcación completa de tipos de objetos en tipos exactos vs no exactos donde un tipo inexacto nunca se puede asignar a uno exacto, incluso si a simple vista son compatibles. El tipo inexacto siempre puede contener más propiedades. (Al menos esta fue una de las razones por las que @ahejlsberg mencionó como tsconf).

Si asExact fuera una forma sintáctica de marcar un objeto tan exacto, así es como se vería dicha solución:

declare const exactMarker: unique symbol 
type IsExact = { [exactMarker]: undefined }
type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

type InexactType = {
    foo: string
}
function asExact<T>(o: T): T & IsExact { 
  return o as T & IsExact;
}

const obj = asExact({
  foo: 'foo',
});


function test1<T extends IsExact & InexactType>(t: Exact<T, InexactType>) {

}

function test2(t: InexactType) {
  test1(t); // error now
}
test2(obj) 
test1(obj);  // ok 

const obj2 = asExact({
  foo: 'foo',
  bar: ""
});
test1(obj2);

const objOpt = asExact < { foo: string, bar?: string }>({
  foo: 'foo',
  bar: ""
});
test1(objOpt);

Enlace de patio de recreo

@dragomirtitian , por eso se me ocurrió la solución un poco antes https://github.com/microsoft/TypeScript/issues/12936#issuecomment -524631270 que no sufre de esto.

@dragomirtitian es una cuestión de cómo escribe sus funciones.
Si lo haces de manera un poco diferente, funciona.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}

function test2<T extends InexactType>(t: T) {
  test1(t); // fails
}
test2(obj)

https://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + + KAXinSgjmAgDsATAZ1wCgooB XMi6 + + k5lt3vygAuKFQgA3CACc o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw + + Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEogl0YsnOA8qgKIKwAmDE5KWgYNckbdcsqSNuD 4oqWgG4oAHoNqGMEGwAbOnTC4EGy3z82oA

@jeremybparagon, su caso está cubierto.

@iamandrewluca Creo que las soluciones aquí y aquí difieren en cómo tratan mi ejemplo .

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: 'a'
}
type C = {
  prop1: string
}

type ShouldBeA = Exact<A, C> // This evaluates to never.

const ob...

Enlace de patio de recreo

@aigoncharov El problema es que debes ser consciente de eso para que uno no pueda hacer esto fácilmente y test1 aún puedan ser llamados con propiedades adicionales. En mi opinión, cualquier solución que pueda permitir tan fácilmente una asignación inexacta accidental ya ha fallado, ya que el objetivo es hacer cumplir la exactitud en el sistema de tipos.

@toriningen sí, su solución parece mejor, solo me refería a la última solución publicada. Su solución tiene a su favor el hecho de que no necesita el parámetro de tipo de función adicional, sin embargo, no parece funcionar bien para las propiedades opcionales:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
type Unexact<T> = T extends Exact<infer R> ? R : T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

////////////////////////////////
// Fixtures:
type Wide = { foo: string, bar?: string };
type Narrow = { foo: string };
type ExactWide = Exact<Wide>;
type ExactNarrow = Exact<Narrow>;

const ew: ExactWide = exact<Wide>({ foo: "", bar: ""});
const assign_en_ew: ExactNarrow = ew; // Ok ? 

Enlace de patio de recreo

@jeremybparagon No estoy seguro de que la solución de @aigoncharov haga un buen trabajo en las propiedades opcionales. Cualquier solución basada en T extends S y S extends T sufrirá por el simple hecho de que

type A = { prop1: string }
type C = { prop1: string,  prop2?: string }
type CextendsA = C extends A ? "Y" : "N" // Y 
type AextendsC = A extends C ? "Y" : "N" // also Y 

Enlace de patio de recreo

Creo que @iamandrewluca de usar Exclude<keyof T, keyof Shape> extends never es bueno, mi tipo es bastante similar (edité mi respuesta original para agregar &R para asegurar T extends R sin cheques adicionales).

type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

Sin embargo, no apostaría mi reputación a que mi solución no tiene agujeros, no los he buscado tanto, pero doy la bienvenida a tales hallazgos 😊

deberíamos tener una bandera donde esto esté habilitado globalmente. De esta forma, quien quiera perder tipografía puede seguir haciendo lo mismo. Demasiados errores causados ​​por este problema. Ahora trato de evitar el operador de propagación y uso pickKeysFromObject(shipDataRequest, ['a', 'b','c'])

Aquí hay un caso de uso para los tipos exactos con los que me topé recientemente:

type PossibleKeys = 'x' | 'y' | 'z';
type ImmutableMap = Readonly<{ [K in PossibleKeys]?: string }>;

const getFriendlyNameForKey = (key: PossibleKeys) => {
    switch (key) {
        case 'x':
            return 'Ecks';
        case 'y':
            return 'Why';
        case 'z':
            return 'Zee';
    }
};

const myMap: ImmutableMap = { x: 'foo', y: 'bar' };

const renderMap = (map: ImmutableMap) =>
    Object.keys(map).map(key => {
        // Argument of type 'string' is not assignable to parameter of type 'PossibleKeys'
        const friendlyName = getFriendlyNameForKey(key);
        // No index signature with a parameter of type 'string' was found on type 'Readonly<{ x?: string | undefined; y?: string | undefined; z?: string | undefined; }>'.    
        return [friendlyName, map[key]];
    });
;

Debido a que los tipos son inexactos de forma predeterminada, Object.keys tiene que devolver un string[] (consulte https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208), pero en este caso , si ImmutableMap era exacto, no hay razón para que no pueda devolver PossibleKeys[] .

@dallonf tenga en cuenta que este ejemplo requiere una funcionalidad adicional además de los tipos exactos: Object.keys es solo una función y debería haber algún mecanismo para describir una función que devuelva keyof T para tipos exactos y string para otros tipos. Simplemente tener la opción de declarar un tipo exacto no sería suficiente.

@RyanCavanaugh Creo que esa fue la implicación, tipos exactos + la capacidad de detectarlos.

Caso de uso para las tipificaciones de reacción:

forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P> .

Es tentador pasar un componente regular a forwardRef razón por la cual React emite advertencias en tiempo de ejecución si detecta propTypes o defaultProps en el argumento render . Nos gustaría expresar esto a nivel de tipo, pero tenemos que recurrir a never :

- forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
+ forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string, propTypes?: never, defaultProps?: never }) => ComponentType<P>

El mensaje de error con never no es útil ("{} no se puede asignar a indefinido").

¿Alguien puede ayudarme sobre cómo se vería la solución de

type StoreEvent =
  | { type: 'STORE_LOADING' }
  | { type: 'STORE_LOADED'; data: unknown[] }

No está claro cómo podría hacer una función dispatch () escrita que solo acepta la forma exacta de un evento.

(ACTUALIZACIÓN: Lo descubrí: https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)

Caso de uso:

La falta de soporte de Exact<> conduce a problemas de tiempo de ejecución con mutaciones GraphQL. GraphQL acepta la lista exacta de propiedades permitidas. Si proporciona demasiados accesorios, arroja un error.

Entonces, cuando obtenemos algunos datos del formulario, Typecript no puede validar propiedades en exceso (extra). Y obtendremos un error en tiempo de ejecución.

El siguiente ejemplo ilustra la seguridad imaginaria

  • en el primer caso rastreó todos los parámetros de entrada
  • pero en la vida real (como el segundo caso cuando obtuvimos datos del formulario y los guardamos en alguna variable)

Probar en el patio de recreo

Screen Shot 2020-03-05 at 13 04 38

De acuerdo con el artículo https://fettblog.eu/typescript-match-the-exact-object-shape/ y soluciones similares proporcionadas anteriormente, podemos usar la siguiente solución fea:

Screen Shot 2020-03-05 at 12 26 57

¿Por qué esta solución savePerson<T>(person: ValidateShape<T, Person>) es fea?

Suponga que tiene un tipo de entrada profundamente anidado, por ejemplo:

// Assume we are in the ideal world where implemented Exact<>

type Person {
  name: string;
  address: Exact<Address>;
}

type Address {
   city: string
   location: Exact<Location>
}

type Location {
   lon: number;
   lat: number; 
}

savePerson(person: Exact<Person>)

No puedo imaginar qué espaguetis deberíamos escribir para obtener el mismo comportamiento con la solución actualmente disponible:

savePerson<T, TT, TTT>(person: 
  ValidateShape<T, Person keyof ...🤯...
     ValidateShape<TT, Address keyof ...💩... 
         ValidateShape<TTT, Location keyof ...🤬... 
> > >)

Entonces, por ahora, tenemos grandes agujeros en el análisis estático en nuestro código, que funciona con datos de entrada anidados complejos.

El caso descrito en la primera imagen, donde TS no valida el exceso de propiedades porque se pierde "frescura", también ha sido un punto doloroso para nosotros.

Escribiendo

doSomething({
  /* large object of options */
})

a menudo se siente mucho menos legible que

const options = {
  /* large object of options */
}
doSomething(options)

Anotar explícitamente const options: DoSomethingOptions = { ayuda, pero es un poco engorroso y difícil de detectar y aplicar en las revisiones de código.

Esta es una idea un poco fuera de tema y no resolvería la mayoría de los casos de uso para la exactitud que se describen aquí, pero ¿sería posible mantener un objeto literal fresco cuando solo se usa una vez dentro del alcance adjunto?

@RyanCavanaugh gracias por explicar EPC ... ¿Se discute la diferencia entre EPC y los tipos exactos con más detalle en alguna parte? Ahora siento que debería comprender mejor por qué EPC permite algunos casos que los tipos exactos no permiten.

Hola @noppa , creo que sería una gran idea. Acabo de tropezar con esto cuando noté la diferencia entre asignar directamente versus asignar a una variable primero, incluso hice una pregunta sobre SO que me trajo aquí. El comportamiento actual es sorprendente, al menos para mí ...

Creo que tengo el mismo problema que el ejemplo de mutaciones GraphQL (escritura anidada exacta, no se deben permitir propiedades adicionales). En mi caso, estoy pensando en escribir respuestas de API en un módulo común (compartido entre frontend y backend):

export type ProductsSlashResponse = {
  products: Array<{
    id: number;
    description: string;
  }>,
  total: number;
};

En el lado del servidor, me gustaría asegurarme de que la respuesta respete ese tipo de firma:

router.get("products/", async () =>
  assertType<ProductsSlashResponse>(getProducts())));

He probado soluciones desde aquí. Una que parece funcionar es T extends U ? U extends T ? T : never : never , junto con una función de curry que no es ideal. El principal problema con esto es que no recibe comentarios sobre propiedades faltantes o adicionales (tal vez podríamos mejorar eso, pero se vuelve difícil de hacer cuando entramos en propiedades anidadas). Otras soluciones no funcionan con objetos profundamente anidados.

Por supuesto, la interfaz generalmente no se bloqueará si envío más información de la especificada, sin embargo, esto podría provocar una fuga de información si la API envía más información de la que debería (y debido a la naturaleza confusa de leer datos de una base de datos qué tipos no están necesariamente sincronizados con el código todo el tiempo, esto podría suceder).

@ fer22f GraphQL no envía campos que el cliente no solicitó ... a menos que esté usando un tipo escalar JSON para products o para los elementos de la matriz, no hay nada de qué preocuparse allí.

Lo siento, leí mal, pensé que querías decir que estabas usando GraphQL

Alguien ya mencionó GraphQL, pero solo en términos de "recopilar casos de uso" ( @DanielRosenwasser mencionó hace varios años en el hilo :-) de "no tener ningún caso de uso"), dos casos de uso en los que quería use Exact son:

  1. Pasar datos a almacenes de datos / bases de datos / ORM: cualquier campo adicional que se pase se eliminará / no se almacenará silenciosamente.

  2. Pasar datos a llamadas electrónicas / RPC / REST / GraphQL: nuevamente, cualquier campo adicional que se pase se eliminará / no enviará silenciosamente.

(Bueno, tal vez no se eliminen silenciosamente, pueden ser errores de tiempo de ejecución).

En ambos casos, me gustaría decirle al programador / a mí mismo (a través de un error de compilación) "... realmente no debería darme esta propiedad adicional, b / c si espera que se 'almacene' o ' enviado ', no será ".

Esto es particularmente necesario en las API de estilo de "actualización parcial", es decir, tipos débiles:

type Data = { firstName:? string; lastName?: string; children?: [{ ... }] };
const data = { firstName: "a", lastNmeTypo: "b" };
await saveDataToDbOrWireCall(data);

Pasa la verificación de tipo débil b / c al menos un parámetro coincidente, firstName , por lo que no es 100% disjunto, sin embargo, todavía hay un error tipográfico "obvio" de lsatNmeTypo que no está siendo detectado.

Por supuesto, EPC funciona si lo hago:

await saveDataToDbOrWireCall({ firstName, lastNmeTypo });

Pero tener que desestructurar + volver a escribir cada campo es bastante tedioso.

Soluciones como Exactify @jcalz funcionan en la propiedad de primer nivel, pero el caso recursivo (es decir, children es una matriz y los elementos de la matriz deben ser exactos) con lo que estoy luchando una vez que llega Casos de uso del "mundo real" con genéricos / como Exact<Foo<Bar<T>> .

Sería genial tener esto incorporado, y solo quería tener en cuenta estos casos de uso explícitos (básicamente llamadas telefónicas con tipos parciales / débiles), si eso ayuda con la priorización / mapa de ruta.

(FWIW https://github.com/stephenh/joist-ts/pull/35/files tiene mi intento actual de un Exact profundo y también un Exact.test.ts que está pasando casos triviales, pero el PR en sí tiene errores de compilación en los usos más esotéricos. Descargo de responsabilidad Realmente no espero que nadie investigue este PR específico, pero lo estoy proporcionando como un "aquí es donde Exact sería útil" + "AFAICT esto es difícil de hacer en el punto de datos de la tierra del usuario).

Oye,

¿Se estaba preguntando qué piensa el equipo de TS con respecto a la propuesta de registros de tipos exactos y tuplas aquí? https://github.com/tc39/proposal-record-tuple

¿Tiene sentido introducir tipos exactos para esas nuevas primitivas?

@slorber No TS, pero eso es ortogonal. Esa propuesta se refiere a la inmutabilidad, y las preocupaciones son casi idénticas entre eso y las bibliotecas como Immutable.js.

Iteé en la versión recursiva de

export type Exact<Expected, Actual> = Expected &
  Actual & // Needed to infer `Actual`
  (null extends Actual
    ? null extends Expected
      ? Actual extends null // If only null stop here, because NonNullable<null> = never
        ? null
        : CheckUndefined<Expected, Actual>
      : never // Actual can be null but not Expected: forbid the field
    : CheckUndefined<Expected, Actual>);

type CheckUndefined<Expected, Actual> = undefined extends Actual
  ? undefined extends Expected
    ? Actual extends undefined // If only undefined stop here, because NonNullable<undefined> = never
      ? undefined
      : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>
    : never // Actual can be undefined but not Expected: forbid the field
  : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>;

type NonNullableExact<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Actual[K] extends (infer ActualElement)[]
      ? Expected[K] extends (infer ExpectedElement)[] | undefined | null
        ? Exact<ExpectedElement, ActualElement>[]
        : never // Not both array
      : Exact<Expected[K], Actual[K]>
    : never; // Forbid extra properties
};

patio de recreo

Exact sería muy útil para nosotros a la hora de devolver respuestas API. Actualmente, esto es lo que resolvemos:

const response = { companies };

res.json(exact<GetCompaniesResponse, typeof response>(response));
export function exact<S, T>(object: Exact<S, T>) {
  return object;
}

Aquí, el tipo Exact es lo que @ArnaudBarre proporcionó anteriormente.

Gracias @ArnaudBarre por desbloquearme y enseñarme algunos ts.
Reflexionando sobre su solución:

export type Exact<Expected, Actual> =
  keyof Expected extends keyof Actual
    ? keyof Actual extends keyof Expected
      ? Expected extends ExactElements<Expected, Actual>
        ? Expected
        : never
      : never
    : never;

type ExactElements<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Expected[K] extends Actual[K]
      ? Actual[K] extends Expected[K]
        ? Expected[K]
        : never
      : never
    : never
};

// should succeed (produce exactly the Expected type)
let s1: Exact< { a: number; b: string }, { a: number; b: string } >;
let s2: Exact< { a?: number; b: string }, { a?: number; b: string } >;
let s3: Exact< { a?: number[]; b: string }, { a?: number[]; b: string } >;
let s4: Exact< string, string >;
let s5: Exact< string[], string[] >;
let s6: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string }[] >;

// should fail (produce never)
let f1: Exact< { a: string; b: string }, { a: number; b: string } >;
let f2: Exact< { a: number; b: string }, { a?: number; b: string } >;
let f3: Exact< { a?: number; b: string }, { a: number; b: string } >;
let f4: Exact< { a: number[]; b: string }, { a: string[]; b: string } >;
let f5: Exact< { a?: number[]; b: string }, { a: number[]; b: string } >;
let f6: Exact< { a?: number; b: string; c: string }, { a?: number; b: string } >;
let f7: Exact< { a?: number; b: string }, { a?: number; b: string; c: string } >;
let f8: Exact< { a?: number; b: string; c?: string }, { a?: number; b: string } >;
let f9: Exact< { a?: number; b: string }, { a?: number; b: string; c?: string } >;
let f10: Exact< never, string >;
let f11: Exact< string, never >;
let f12: Exact< string, number >;
let f13: Exact< string[], string >;
let f14: Exact< string, string[] >;
let f15: Exact< string[], number[] >;
let f16: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string } >;

La solución anterior tuvo éxito para f6, f8 y f9.
Esta solución también devuelve resultados "más limpios"; cuando coincide, recupera el tipo 'Esperado'.
Al igual que con el comentario de @ArnaudBarre ... no estoy seguro de si se manejan todos los casos extremos, así que ymmv ...

@heystewart Tu Exact no da un resultado simétrico:

let a: Exact< { foo: number }[], { foo: number, bar?: string }[] >;
let b: Exact< { foo: number, bar?: string }[], { foo: number }[] >;

a = [{ foo: 123, bar: 'bar' }]; // error
b = [{ foo: 123, bar: 'bar' }]; // no error

Editar: la versión de @ArnaudBarre también tiene el mismo problema

@papb Sí, efectivamente, mi escritura no funciona si el punto de entrada es una matriz. Lo necesitaba para nuestra API graphQL, donde variables siempre es un objeto.

Para resolverlo, debe aislar ExactObject y ExactArray y tener un punto de entrada que vaya a uno u otro.

Entonces, ¿cuál es la mejor manera de asegurarse de que el objeto tenga propiedades exactas, ni menos, ni más?

@ captain-yossarian convence al equipo de TypeScript para que implemente esto. Ninguna solución presentada aquí funciona para todos los casos esperados y casi todos carecen de claridad.

@toriningen no puede imaginar cuántos problemas se resolverán si el equipo de TS implementará esta función

@RyanCavanaugh
En la actualidad, tengo un caso de uso que me trajo aquí, y se encuentra directamente con su tema "Miscelánea". Quiero una función que:

  1. toma un parámetro que implementa una interfaz con parámetros opcionales
  2. devuelve un objeto escrito a la interfaz real más estrecha del parámetro dado, de modo que

Esos objetivos inmediatos sirven a estos fines:

  1. Recibo un exceso de propiedad comprobando la entrada.
  2. Obtengo autocompletado y seguridad de tipo de propiedad para la salida

Ejemplo

He reducido mi caso a esto:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function y<
    Y extends X
>(
    y: (X extends Y ? Y : X)
) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

Esa configuración funciona y logra todos los objetivos deseados, por lo que desde un punto de vista de factibilidad pura y en lo que respecta a esto, estoy bien. Sin embargo, el EPC se logra mediante el parámetro escribiendo (X extends Y ? Y : X) . Básicamente me topé con eso por casualidad, y me sorprendió un poco que funcionara.

Propuesta

Y es por eso que me gustaría tener una palabra clave implements que se pueda usar en lugar de extends para marcar la intención de que el tipo aquí no tenga propiedades en exceso. Al igual que:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function x<
    Y implements X
>( y: Y ) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

Esto me parece mucho más claro que mi solución actual. Además de ser más conciso, ubica toda la restricción con la declaración de genéricos en lugar de mi división actual entre los genéricos y los parámetros.

Eso también puede permitir más casos de uso que actualmente son imposibles o imprácticos, pero que actualmente es solo una intuición.

Detección de tipo débil como alternativa

En particular, la Detección de tipo débil según # 3842 debería solucionarlo igual de bien, y podría ser favorable debido a que no requiere sintaxis adicional, si funcionó en relación con extends , según mi caso de uso.

Con respecto a Exact<Type> etc.

Finalmente, implements , como lo imagino, debería ser bastante sencillo con respecto a su punto sobre function f<T extends Exact<{ n: number }>(p: T) ya que no intenta resolver el caso más general de Exact<Type> .

En general, Exact<Type> parece ser de poca utilidad junto con EPC, y no puedo imaginar un caso válido generalmente útil que quede fuera de estos grupos:

  • llamadas de función: estas se pueden manejar fácilmente ahora según mi ejemplo, y se beneficiarían de implements
  • asignaciones: solo use literales, por lo que se aplica EPC
  • datos que están fuera de su dominio de control: la verificación de tipos no puede protegerlo contra eso, debe manejar eso en tiempo de ejecución, momento en el que regresa a transmisiones seguras

Obviamente, habrá casos en los que no pueda asignar literales, pero estos también deberían ser de un conjunto finito:

  • si se le dan los datos de asignación en una función, maneje la verificación de tipo en la firma de la llamada
  • si fusiona varios objetos, según OP, entonces afirme el tipo de cada objeto de origen correctamente y puede lanzar de forma segura as DesiredType

Resumen: implements estaría bien, pero por lo demás estamos bien

En resumen, estoy seguro de que con implements y la reparación de EPC (si surgen problemas), los tipos exactos realmente deberían manejarse.

Pregunta a todas las partes interesadas: ¿hay algo realmente abierto aquí?

Habiendo examinado los casos de uso aquí, creo que casi todos los repros se manejan correctamente a estas alturas, y el resto se puede hacer funcionar con mi pequeño ejemplo anterior. Eso plantea la pregunta: ¿alguien todavía tiene problemas relacionados con esto hoy con TS actualizado?

Tengo una idea inmadura sobre las anotaciones de tipo. Emparejar un objeto que se divide en miembros puede ser exactamente igual, ni más ni menos, más o menos, ni más ni menos, más pero no menos. Para cada uno de los casos anteriores, debe haber una expresión.

exactamente igual, es decir, ni más ni menos:

function foo(p:{|x:any,y:any|})

//it matched 
foo({x,y})
//no match
foo({x})
foo({y})
foo({x,y,z})
foo({})

más pero no menos:

function foo(p:{|x:any,y:any, ...|})

//it matched 
foo({x,y})
foo({x,y,z})

//no matched
foo({x})
foo({y})
foo({x,z})

ni más pero menos:

function foo(p:{x:any,y:any})

//it matched 
foo({x,y})
foo({x})
foo({y})

//no match
foo({x,z})
foo({x,y,z})

más o menos:

function foo(p:{x:any,y:any, ...})

//it matched 
foo({x,y})
foo({x})
foo({y})
foo({x,z})
foo({x,y,z})

conclusión:

Con una línea vertical indica que no hay menos, sin una línea vertical significa que puede haber menos. Con un signo de elipsis significa que puede haber más, sin un signo de elipsis significa que no puede haber más. La coincidencia de matrices es la misma idea.

function foo(p:[|x,y|]) // p.length === 2
function foo(p:[|x,y, ... |]) // p.length >= 2
function foo(p:[x,y]) // p.length >= 0
function foo(p:[x,y,...]) // p.length >= 0

@rasenplanscher usando su ejemplo, esto compila:

const x = { blue: 1, red: 3, purple: 4 };
const z = y(x);

Sin embargo, con tipos exactos, no debería. Es decir, la pregunta aquí es no depender de EPC.

@ xp44mm "más pero no menos" ya es el comportamiento y "más o menos" es el comportamiento si marca todas las propiedades como opcionales

function foo(p:{x?: any, y?: any}) {}
const x = 1, y = 1, z = 1
// all pass
foo({x,y})
foo({x})
foo({y})
const p1 = {x,z}
foo(p1)
const p2 = {x,y,z}
foo(p2)

De manera similar, si tuviéramos tipos exactos, el tipo exacto + todas las propiedades opcionales serían esencialmente "no más, pero menos".

Otro ejemplo de este problema. Creo que es una buena demostración de esta propuesta. En este caso, uso rxjs para trabajar con Subject pero quiero devolver un Observable ("bloqueado") (que no tiene un método next , error , etc. para manipular el valor.)

someMethod(): Observable<MyType> {
  const subject = new Subject<MyType>();

  // This works, but should not. (if this proposal is implemented.)
  return subject;

  // Only Observable should be allowed as return type.
  return subject.asObservable();
}

Siempre quiero devolver solo el tipo exacto Observable y no Subject que lo extiende.

Propuesta:

// Adding exclamation mark `!` (or something else) to match exact type. (or some other position `method(): !Foo`, ...)
someMethod()!: Observable<MyType> {
  // ...
}

Pero estoy seguro de que tienes mejores ideas. Especialmente porque esto no solo afecta los valores de retorno, ¿verdad? De todos modos, solo una demostración de pseudocódigo. Creo que sería una buena característica para evitar errores y carencias. Como en el caso descrito anteriormente. Otra solución podría ser agregar un nuevo tipo de utilidad .
¿O me perdí algo? ¿Esto ya funciona? Yo uso TypeScript 4.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

blendsdk picture blendsdk  ·  3Comentarios

Roam-Cooper picture Roam-Cooper  ·  3Comentarios

MartynasZilinskas picture MartynasZilinskas  ·  3Comentarios

wmaurer picture wmaurer  ·  3Comentarios

fwanicka picture fwanicka  ·  3Comentarios