Typescript: Types exacts

Créé le 15 déc. 2016  ·  171Commentaires  ·  Source: microsoft/TypeScript

Il s'agit d'une proposition pour permettre une syntaxe pour les types exacts. Une fonctionnalité similaire peut être vue dans Flow (https://flowtype.org/docs/objects.html#exact-object-types), mais je voudrais la proposer en tant que fonctionnalité utilisée pour les littéraux de type et non les interfaces. La syntaxe spécifique que je proposerais d'utiliser est le tuyau (qui reflète presque l'implémentation de Flow, mais il devrait entourer l'instruction type), car il est familier comme la syntaxe mathématique absolue.

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.

Ce changement de syntaxe serait une nouvelle fonctionnalité et affecterait les nouveaux fichiers de définition en cours d'écriture s'il était utilisé comme paramètre ou type exposé. Cette syntaxe pourrait être combinée avec d'autres types plus complexes.

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

type Bar = Exact<{ username: string }>

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

Je m'excuse d'avance s'il s'agit d'un doublon, je n'arrivais pas à trouver les bons mots-clés pour trouver des doublons de cette fonctionnalité.

Edit : Ce message a été mis à jour pour utiliser la proposition de syntaxe préférée mentionnée sur https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-267272371 , qui englobe l'utilisation d'une syntaxe plus simple avec un type générique pour permettre l'utilisation dans les expressions.

Awaiting More Feedback Suggestion

Commentaire le plus utile

Nous en avons parlé pendant un bon moment. Je vais essayer de résumer la discussion.

Vérification des propriétés excédentaires

Les types exacts ne sont qu'un moyen de détecter des propriétés supplémentaires. La demande de types exacts a beaucoup diminué lorsque nous avons initialement mis en œuvre la vérification des propriétés excessives (EPC). L'EPC a probablement été le plus gros changement que nous ayons apporté, mais il a porté ses fruits ; presque immédiatement, nous avons eu des bogues lorsque EPC n'a pas détecté de propriété en excès.

Pour la plupart, lorsque les gens veulent des types exacts, nous préférerions résoudre ce problème en rendant EPC plus intelligent. Un domaine clé ici est lorsque le type cible est un type union - nous voulons simplement considérer cela comme une correction de bogue (EPC devrait fonctionner ici mais il n'est pas encore implémenté).

Types tout facultatifs

Le problème des types tout facultatifs (que j'appelle les types "faibles") est lié à EPC. Très probablement, tous les types faibles voudraient être exacts. Nous devrions juste implémenter la détection de type faible (#7485 / #3842) ; le seul bloqueur ici est les types d'intersection qui nécessitent une complexité supplémentaire dans la mise en œuvre.

Quel type est exact ?

Le premier problème majeur que nous voyons avec les types exacts est qu'il n'est vraiment pas clair quels types doivent être marqués comme exacts.

À une extrémité du spectre, vous avez des fonctions qui lèveront littéralement une exception (ou feront de mauvaises choses) si on leur donne un objet avec une clé propre en dehors d'un domaine fixe. Ceux-ci sont rares (je ne peux pas citer d'exemple de mémoire). Au milieu, il y a des fonctions qui ignorent silencieusement
propriétés inconnues (presque toutes). Et à l'autre extrémité, vous avez des fonctions qui opèrent de manière générique sur toutes les propriétés (par exemple Object.keys ).

Il est clair que les fonctions "lancera si des données supplémentaires sont données" doivent être marquées comme acceptant des types exacts. Mais qu'en est-il du milieu ? Les gens seront probablement en désaccord. Point2D / Point3D est un bon exemple - vous pouvez raisonnablement dire qu'une fonction magnitude doit avoir le type (p: exact Point2D) => number pour éviter de passer un Point3D . Mais pourquoi ne puis-je pas passer mon objet { x: 3, y: 14, units: 'meters' } à cette fonction ? C'est là qu'EPC entre en jeu - vous voulez détecter cette propriété "supplémentaire" units dans des emplacements où elle est définitivement supprimée, mais ne bloquez pas réellement les appels qui impliquent un alias.

Violations des hypothèses/problèmes d'instanciation

Nous avons des principes de base que les types exacts invalideraient. Par exemple, on suppose qu'un type T & U est toujours assignable à T , mais cela échoue si T est un type exact. C'est problématique car vous pouvez avoir une fonction générique qui utilise ce principe T & U -> T , mais invoquez la fonction avec T instanciée avec un type exact. Il n'y a donc aucun moyen de faire ce son (ce n'est vraiment pas acceptable d'erreur sur l'instanciation) - pas nécessairement un bloqueur, mais c'est déroutant d'avoir une fonction générique plus permissive qu'une version instanciée manuellement d'elle-même !

On suppose également que T est toujours assignable à T | U , mais il n'est pas évident d'appliquer cette règle si U est un type exact. Est-ce que { s: "hello", n: 3 } assignable à { s: string } | Exact<{ n: number }> ? "Oui" semble être la mauvaise réponse parce que celui qui cherche n et trouve qu'il ne sera pas heureux de voir s , mais "Non" semble également faux parce que nous avons violé le T -> T | U base

Recueil

Quelle est la signification de function f<T extends Exact<{ n: number }>(p: T) ? :confus:

Souvent, des types exacts sont souhaités là où ce que vous voulez vraiment est une union "auto-disjointe". En d'autres termes, vous pourriez avoir une API qui peut accepter { type: "name", firstName: "bob", lastName: "bobson" } ou { type: "age", years: 32 } mais ne voulez pas accepter { type: "age", years: 32, firstName: 'bob" } car quelque chose d'imprévisible va se produire. Le type "correct" est sans doute { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } mais bon, c'est ennuyeux à taper. Nous pourrions potentiellement penser au sucre pour créer des types comme celui-ci.

Résumé : cas d'utilisation nécessaires

Notre diagnostic optimiste est qu'il s'agit, en dehors des relativement peu d'API vraiment fermées, d'une solution au problème XY . Dans la mesure du possible, nous devrions utiliser EPC pour détecter les "mauvaises" propriétés. Donc, si vous avez un problème et que vous pensez que les types exacts sont la bonne solution, veuillez décrire le problème d'origine ici afin que nous puissions composer un catalogue de modèles et voir s'il existe d'autres solutions qui seraient moins invasives/déroutantes.

Tous les 171 commentaires

Je dirais que la syntaxe est discutable ici. Étant donné que TypeScript autorise désormais le canal principal pour le type d'union.

class B {}

type A = | number | 
B

Compile maintenant et équivaut à type A = number | B , grâce à l'insertion automatique de point-virgule.

Je pense que cela pourrait ne pas m'attendre si le type exact est introduit.

Je ne sais pas si c'est réel mais pour info https://github.com/Microsoft/TypeScript/issues/7481

Si la syntaxe {| ... |} était adoptée, nous pourrions construire sur des types mappés afin que vous puissiez écrire

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

et ensuite vous pourriez écrire Exact<User> .

C'est probablement la dernière chose qui me manque de Flow, par rapport à TypeScript.

L'exemple Object.assign est particulièrement bon. Je comprends pourquoi TypeScript se comporte comme il le fait aujourd'hui, mais la plupart du temps, je préfère avoir le type exact.

@HerringtonDarkholme Merci. Mon problème initial l'a mentionné, mais je l'ai omis à la fin car quelqu'un aurait une meilleure syntaxe de toute façon, s'avère que oui 😄

@DanielRosenwasser Cela semble beaucoup plus raisonnable, merci!

@wallverb Je ne pense pas, même si j'aimerais aussi voir cette fonctionnalité exister 😄

Que se passe-t-il si je veux exprimer une union de types, où certains d'entre eux sont exacts et d'autres non ? La syntaxe suggérée la rendrait sujette aux erreurs et difficile à lire, même si une attention particulière est accordée à l'espacement :

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

Pouvez-vous dire rapidement quels membres du syndicat ne sont pas exacts?

Et sans l'espacement prudent?

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

(réponse : Type3 , Type5 )

@rotemdan Voir la réponse ci-dessus, il y a le type générique Extact place qui est une proposition plus solide que la mienne. Je pense que c'est l'approche préférée.

Il y a aussi le souci de savoir à quoi cela ressemblerait dans les conseils de l'éditeur, les fenêtres contextuelles de prévisualisation et les messages du compilateur. Les alias de type "s'aplatissent" actuellement en expressions de type brut. L'alias n'est pas conservé, de sorte que les expressions incompréhensibles apparaîtront toujours dans l'éditeur, à moins que des mesures spéciales ne soient appliquées pour contrer cela.

J'ai du mal à croire que cette syntaxe a été acceptée dans un langage de programmation comme Flow, qui a des unions avec la même syntaxe que Typescript. Pour moi, il ne semble pas sage d'introduire une syntaxe défectueuse qui est fondamentalement en conflit avec la syntaxe existante, puis d'essayer très fort de la "couvrir".

Une alternative intéressante (amusante ?) consiste à utiliser un modificateur comme only . J'ai eu un brouillon de proposition pour cela il y a plusieurs mois, je pense, mais je ne l'ai jamais soumis :

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

C'était la meilleure syntaxe que j'ai pu trouver à l'époque.

_Edit_ : just pourrait également fonctionner ?

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

_(Edit : maintenant que je me souviens que la syntaxe était à l'origine pour un modificateur pour les types nominaux, mais je suppose que cela n'a pas vraiment d'importance.. Les deux concepts sont suffisamment proches pour que ces mots-clés puissent également fonctionner ici)_

Je me demandais, peut-être que les deux mots-clés pourraient être introduits pour décrire deux types de correspondance légèrement différents :

  • just T (ce qui signifie : "exactement T ") pour une correspondance structurelle exacte, comme décrit ici.
  • only T (signification : "uniquement T ") pour une correspondance nominale.

L'appariement nominal pourrait être considéré comme une version encore plus « stricte » de l'appariement structurel exact. Cela signifierait que non seulement le type doit être structurellement identique, mais que la valeur elle-même doit être associée exactement au même identificateur de type que celui spécifié. Cela peut prendre en charge ou non les alias de type, en plus des interfaces et des classes.

Personnellement, je ne pense pas que la différence subtile créerait autant de confusion, même si je pense qu'il appartient à l'équipe Typescript de décider si le concept d'un modificateur nominal comme only semble approprié. Je suggère seulement ceci comme une option.

_(Edit : juste une note à propos de only lorsqu'il est utilisé avec des classes : il y a ici une ambiguïté quant à savoir si cela permettrait des sous-classes nominales lorsqu'une classe de base est référencée - cela doit être discuté séparément, je suppose. degré moindre - la même chose pourrait être envisagée pour les interfaces - bien que je ne pense pas actuellement que ce serait si utile)_

Cela ressemble à des types de soustraction déguisés. Ces problèmes peuvent être pertinents : https://github.com/Microsoft/TypeScript/issues/4183 https://github.com/Microsoft/TypeScript/issues/7993

@ethanresnick Pourquoi crois-tu ça ?

Ce serait extrêmement utile dans la base de code sur laquelle je travaille en ce moment. Si cela faisait déjà partie du langage, je n'aurais pas passé la journée à rechercher une erreur.

(Peut-être d'autres erreurs mais pas cette erreur en particulier )

Je n'aime pas la syntaxe pipe inspirée de Flow. Quelque chose comme le mot-clé exact derrière les interfaces serait plus facile à lire.

exact interface Foo {}

@mohsen1 Je suis sûr que la plupart des gens utiliseraient le type générique Exact dans les positions d'expression, donc cela ne devrait pas trop avoir d'importance. Cependant, je serais préoccupé par une proposition comme celle-ci, car vous risquez de surcharger prématurément la gauche du mot-clé d'interface qui était auparavant réservé aux seules exportations (en étant cohérent avec les valeurs JavaScript - par exemple export const foo = {} ). Cela indique également que ce mot-clé est peut-être également disponible pour les types (par exemple, exact type Foo = {} et maintenant ce sera export exact interface Foo {} ).

Avec la syntaxe {| |} comment extends fonctionnerait-il ? interface Bar extends Foo {| |} sera-t-il exact si Foo n'est pas exact ?

Je pense que le mot-clé exact permet de savoir facilement si une interface est exacte. Cela peut (devrait ?) fonctionner pour type aussi.

interface Foo {}
type Bar = exact Foo

Extrêmement utile pour les choses qui fonctionnent sur des bases de données ou des appels réseau vers des bases de données ou des SDK comme AWS SDK qui prennent des objets avec toutes les propriétés facultatives car les données supplémentaires sont ignorées en silence et peuvent conduire à des bogues difficiles à très difficiles à trouver :rose:

@mohsen1 Cette question ne semble pas pertinente pour la syntaxe, car la même question existe toujours en utilisant l'approche par mot-clé. Personnellement, je n'ai pas de réponse préférée et je devrais jouer avec les attentes existantes pour y répondre - mais ma réaction initiale est que peu importe que Foo soit exact ou non.

L'utilisation d'un mot-clé exact semble ambigu - vous dites qu'il peut être utilisé comme exact interface Foo {} ou type Foo = exact {} ? Que signifie exact Foo | Bar ? L'utilisation de l'approche générique et le travail avec des modèles existants signifient qu'aucune réinvention ou apprentissage n'est requis. C'est juste interface Foo {||} (c'est la seule nouveauté ici), puis type Foo = Exact<{}> et Exact<Foo> | Bar .

Nous en avons parlé pendant un bon moment. Je vais essayer de résumer la discussion.

Vérification des propriétés excédentaires

Les types exacts ne sont qu'un moyen de détecter des propriétés supplémentaires. La demande de types exacts a beaucoup diminué lorsque nous avons initialement mis en œuvre la vérification des propriétés excessives (EPC). L'EPC a probablement été le plus gros changement que nous ayons apporté, mais il a porté ses fruits ; presque immédiatement, nous avons eu des bogues lorsque EPC n'a pas détecté de propriété en excès.

Pour la plupart, lorsque les gens veulent des types exacts, nous préférerions résoudre ce problème en rendant EPC plus intelligent. Un domaine clé ici est lorsque le type cible est un type union - nous voulons simplement considérer cela comme une correction de bogue (EPC devrait fonctionner ici mais il n'est pas encore implémenté).

Types tout facultatifs

Le problème des types tout facultatifs (que j'appelle les types "faibles") est lié à EPC. Très probablement, tous les types faibles voudraient être exacts. Nous devrions juste implémenter la détection de type faible (#7485 / #3842) ; le seul bloqueur ici est les types d'intersection qui nécessitent une complexité supplémentaire dans la mise en œuvre.

Quel type est exact ?

Le premier problème majeur que nous voyons avec les types exacts est qu'il n'est vraiment pas clair quels types doivent être marqués comme exacts.

À une extrémité du spectre, vous avez des fonctions qui lèveront littéralement une exception (ou feront de mauvaises choses) si on leur donne un objet avec une clé propre en dehors d'un domaine fixe. Ceux-ci sont rares (je ne peux pas citer d'exemple de mémoire). Au milieu, il y a des fonctions qui ignorent silencieusement
propriétés inconnues (presque toutes). Et à l'autre extrémité, vous avez des fonctions qui opèrent de manière générique sur toutes les propriétés (par exemple Object.keys ).

Il est clair que les fonctions "lancera si des données supplémentaires sont données" doivent être marquées comme acceptant des types exacts. Mais qu'en est-il du milieu ? Les gens seront probablement en désaccord. Point2D / Point3D est un bon exemple - vous pouvez raisonnablement dire qu'une fonction magnitude doit avoir le type (p: exact Point2D) => number pour éviter de passer un Point3D . Mais pourquoi ne puis-je pas passer mon objet { x: 3, y: 14, units: 'meters' } à cette fonction ? C'est là qu'EPC entre en jeu - vous voulez détecter cette propriété "supplémentaire" units dans des emplacements où elle est définitivement supprimée, mais ne bloquez pas réellement les appels qui impliquent un alias.

Violations des hypothèses/problèmes d'instanciation

Nous avons des principes de base que les types exacts invalideraient. Par exemple, on suppose qu'un type T & U est toujours assignable à T , mais cela échoue si T est un type exact. C'est problématique car vous pouvez avoir une fonction générique qui utilise ce principe T & U -> T , mais invoquez la fonction avec T instanciée avec un type exact. Il n'y a donc aucun moyen de faire ce son (ce n'est vraiment pas acceptable d'erreur sur l'instanciation) - pas nécessairement un bloqueur, mais c'est déroutant d'avoir une fonction générique plus permissive qu'une version instanciée manuellement d'elle-même !

On suppose également que T est toujours assignable à T | U , mais il n'est pas évident d'appliquer cette règle si U est un type exact. Est-ce que { s: "hello", n: 3 } assignable à { s: string } | Exact<{ n: number }> ? "Oui" semble être la mauvaise réponse parce que celui qui cherche n et trouve qu'il ne sera pas heureux de voir s , mais "Non" semble également faux parce que nous avons violé le T -> T | U base

Recueil

Quelle est la signification de function f<T extends Exact<{ n: number }>(p: T) ? :confus:

Souvent, des types exacts sont souhaités là où ce que vous voulez vraiment est une union "auto-disjointe". En d'autres termes, vous pourriez avoir une API qui peut accepter { type: "name", firstName: "bob", lastName: "bobson" } ou { type: "age", years: 32 } mais ne voulez pas accepter { type: "age", years: 32, firstName: 'bob" } car quelque chose d'imprévisible va se produire. Le type "correct" est sans doute { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } mais bon, c'est ennuyeux à taper. Nous pourrions potentiellement penser au sucre pour créer des types comme celui-ci.

Résumé : cas d'utilisation nécessaires

Notre diagnostic optimiste est qu'il s'agit, en dehors des relativement peu d'API vraiment fermées, d'une solution au problème XY . Dans la mesure du possible, nous devrions utiliser EPC pour détecter les "mauvaises" propriétés. Donc, si vous avez un problème et que vous pensez que les types exacts sont la bonne solution, veuillez décrire le problème d'origine ici afin que nous puissions composer un catalogue de modèles et voir s'il existe d'autres solutions qui seraient moins invasives/déroutantes.

Le principal endroit où je vois que les gens sont surpris de n'avoir aucun type d'objet exact est dans le comportement de Object.keys et for..in -- ils produisent toujours un type string au lieu de 'a'|'b' pour quelque chose de tapé { a: any, b: any } .

Comme je l'ai mentionné dans https://github.com/Microsoft/TypeScript/issues/14094 et que vous avez décrit dans la section Miscellany, il est ennuyeux que {first: string, last: string, fullName: string} conforme à {first: string; last: string} | {fullName: string} .

Par exemple, on suppose qu'un type T & U est toujours assignable à T, mais cela échoue si T est un type exact

Si T est un type exact, alors probablement T & U est never (ou T === U ). Droit?

Ou U est un sous-ensemble non exact de T

Mon cas d'utilisation qui m'a conduit à cette suggestion sont les réducteurs de 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
   }
}

Comme vous l'avez souligné dans le résumé, mon problème n'est pas directement que j'ai besoin d'interfaces exactes, j'ai besoin que l'opérateur de propagation fonctionne avec précision. Mais comme le comportement de l'opérateur spread est donné par JS, la seule solution qui me vient à l'esprit est de définir le type de retour ou l'interface pour être exact.

Ai-je bien compris qu'attribuer une valeur de T à Exact<T> serait une erreur ?

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

Dans cet exemple, réduire Dog à Exact<Dog> ne serait pas sûr, n'est-ce pas ?
Considérez cet exemple :

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 Oui, ce serait l'idée. Vous ne pouvez affecter que Exact<T> à Exact<T> . Mon cas d'utilisation immédiat est que les fonctions de validation géreraient les types Exact (par exemple, en prenant les charges utiles des requêtes en tant que any et en produisant des Exact<T> valides). Exact<T> , cependant, serait attribuable à T .

@nerumo

Comme vous l'avez souligné dans le résumé, mon problème n'est pas directement que j'ai besoin d'interfaces exactes, j'ai besoin que l'opérateur de propagation fonctionne avec précision. Mais comme le comportement de l'opérateur spread est donné par JS, la seule solution qui me vient à l'esprit est de définir le type de retour ou l'interface pour être exact.

J'ai rencontré le même problème et j'ai trouvé cette solution qui pour moi est une solution de contournement assez élégante :)

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;
}

vous pouvez en trouver plus dans cette section de mon guide de redux :

Notez que cela pourrait être résolu dans l'espace utilisateur en utilisant ma proposition de types de contraintes (#13257) :

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

Edit : mise à jour de la syntaxe relative à la proposition

@piotrwitek merci, l'astuce Partial fonctionne parfaitement et a déjà trouvé un bug dans ma base de code ;) ça vaut le petit code passe-partout. Mais je suis toujours d'accord avec @isiahmeadows qu'un Exactserait encore mieux

@piotrwitek en utilisant Partial comme ça _presque_ a résolu mon problème, mais cela permet toujours aux propriétés de devenir indéfinies même si l'interface State prétend qu'elles ne le sont pas (je suppose strictNullChecks).

Je me suis retrouvé avec quelque chose d'un peu plus complexe pour préserver les types d'interface :

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 c'est correct, la solution acceptera undefined, mais de mon point de vue, c'est acceptable, car dans mes solutions, j'utilise uniquement des créateurs d'action avec les paramètres requis pour la charge utile, et cela garantira qu'aucune valeur indéfinie ne devrait jamais être affecté à une propriété non nullable.
En pratique, j'utilise cette solution depuis un certain temps en production et ce problème ne s'est jamais produit, mais faites-moi part de vos préoccupations.

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' }

DEMO - activez strictNullChecks dans les options

vous pouvez également créer une charge utile nullable si vous en avez besoin, vous pouvez en lire plus dans mon guide : https://github.com/piotrwitek/react-redux-typescript-guide#actions

Lorsque les types de repos sont fusionnés, cette fonctionnalité peut facilement être transformée en sucre syntaxique sur eux.

Proposition

La logique d'égalité des types doit être rendue stricte - seuls les types avec les mêmes propriétés ou les types qui ont des propriétés de repos qui peuvent être instanciées de manière à ce que leurs types parents aient les mêmes propriétés sont considérés comme correspondants. Pour préserver la compatibilité descendante, un type de repos synthétique est ajouté à tous les types, sauf s'il en existe déjà un. Un nouveau drapeau --strictTypes est également ajouté, qui supprime l'ajout de paramètres de repos synthétiques.

Égalité sous --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 n'est pas activé, une propriété ...rest: <T>T est automatiquement ajoutée sur le type A . Ainsi les lignes a = b; et a = c; ne seront plus des erreurs, comme c'est le cas avec la variable b sur les deux lignes qui suivent.

Un mot sur les violations des hypothèses

on suppose qu'un type T & U est toujours assignable à T, mais cela échoue si T est un type exact.

Oui, & permet une fausse logique mais c'est le cas avec string & number . Tant string que number sont des types rigides distincts qui ne peuvent pas être croisés, cependant le système de types le permet. Les types exacts sont également rigides, de sorte que l'incohérence est toujours cohérente. Le problème réside dans l'opérateur & - il n'est pas solide.

Est-ce que { s: "hello", n: 3 } peut être affecté à { s: string } | Exact<{ n : nombre }>.

Cela peut se traduire par :

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

La réponse devrait donc être « oui ». L'union exacte avec des types non-exacts n'est pas sûre, car les types non-exacts englobent tous les types exacts à moins qu'une propriété discriminante ne soit présente.

Re : la fonction f<T extends Exact<{ n: number }>(p: T) dans le commentaire de @RyanCavanaugh ci-dessus, dans l'une de mes bibliothèques, j'aimerais beaucoup implémenter la fonction suivante :

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

C'est-à-dire une fonction qui renvoie son paramètre avec exactement le même type, mais en même temps vérifie également si son type est également exactement du même type qu'un autre (T).

Voici un exemple un peu artificiel avec trois de mes tentatives infructueuses pour satisfaire les deux exigences :

  1. Aucune propriété excédentaire par rapport à CorrectObject
  2. Affectable à HasX sans spécifier HasX comme type d'objet
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
}

Ici, HasX est un type grandement simplifié (le type réel correspond à un type de schéma) qui est défini dans une couche différente de la constante elle-même, je ne peux donc pas créer le type de o être ( CorrectObject & HasX ).

Avec les types exacts, la solution serait :

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 est un type exact, alors T & U ne l'est probablement jamais (ou T === U). Droit?

Je pense que T & U devrait être never seulement si U est incompatible avec T , par exemple si T est Exact<{x: number | string}> et U est {[field: string]: number} , alors T & U devrait être Exact<{x: number}>

Voir la première réponse à cela :

Ou U est un sous-ensemble non exact de T

Je dirais que si U est attribuable à T, alors T & U === T . Mais si T et U sont des types exacts différents, alors T & U === never .

Dans votre exemple, pourquoi est-il nécessaire d'avoir une fonction checkType qui ne fait rien ? Pourquoi ne pas simplement avoir const o: Exact<CorrectObject> = { ... } ?

Parce qu'il perd l'information que x existe définitivement (facultatif dans CorrectObject) et est un nombre (nombre | chaîne dans CorrectObject). Ou peut-être que j'ai mal compris ce que signifie Exact, je pensais que cela empêcherait simplement les propriétés étrangères, pas que cela signifierait de manière récurrente que tous les types doivent être exactement les mêmes.

Une autre considération dans la prise en charge des types exacts et par rapport à l'EPC actuel est la refactorisation - si la refactorisation de la variable d'extraction était disponible, on perdrait l'EPC à moins que la variable extraite n'introduise une annotation de type, ce qui pourrait devenir très détaillé.

Pour clarifier pourquoi je soutiens les types exacts - ce n'est pas pour les unions discriminées mais les fautes d'orthographe et les propriétés étrangères à tort au cas où le type costraint ne peut pas être spécifié en même temps que le littéral d'objet.

@andy-ms

Je dirais, si U est assignable à T, alors T & U === T. Mais si T et U sont des types exacts différents, alors T & U === jamais.

L'opérateur de type & est un opérateur d'intersection, le résultat en est le sous-ensemble commun des deux côtés, qui n'est pas nécessairement égal non plus. Exemple le plus simple auquel je puisse penser :

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

ici T & U devrait être Exact<{ x: any, y: any }> , qui est un sous-ensemble de T et de U , mais ni T n'est un sous-ensemble de U (manquant x) ni U est un sous-ensemble de T (manquant y).

Cela devrait fonctionner indépendamment du fait que T , U , ou T & U soient des types exacts.

@magnushiie Vous avez un bon point - les types exacts peuvent limiter l'assignabilité des types avec une plus grande largeur, mais permettent toujours l'assignation des types avec une plus grande profondeur. Vous pouvez donc croiser Exact<{ x: number | string }> avec Exact<{ x: string | boolean }> pour obtenir Exact<{ x: string }> . Un problème est que ce n'est pas réellement sécurisé si x n'est pas en lecture seule -- nous pourrions vouloir corriger cette erreur pour les types exacts, car ils signifient opter pour un comportement plus strict.

Les types exacts peuvent également être utilisés pour les problèmes de relations d'arguments de type afin d'indexer les signatures.

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

Voici un moyen intuitif de vérifier le type exact :

// 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'.

Malheureusement, il n'est actuellement pas possible de faire l'assertion Exact en tant que paramètre de type, elle doit donc être faite pendant l'appel (c'est-à-dire que vous devez vous en souvenir).

Voici les utilitaires d'assistance nécessaires pour le faire fonctionner (merci à @tycho01 pour certains d'entre eux):

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];

Voir : Aire de jeux .

Joli! @gcanti ( typelevel-ts ) et @pelotom ( type-zoo ) pourraient également être intéressés. :)

Pour toute personne intéressée, j'ai trouvé un moyen simple d'appliquer des types exacts sur les paramètres de fonction. Fonctionne sur TS 2.7, au moins.

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

EDIT : je suppose que pour que cela fonctionne, vous devez spécifier un objet littéral directement dans l'argument ; cela ne fonctionne pas si vous déclarez un const séparé ci-dessus et le transmettez à la place. :/ Mais une solution de contournement consiste à utiliser simplement la propagation d'objets sur le site d'appel, c'est-à-dire myFn({...arg}) .

EDIT : désolé, je n'ai pas lu que vous avez mentionné TS 2.7 uniquement. je vais le tester là bas !

@vaskevich Je n'arrive pas à le faire fonctionner, c'est-à-dire qu'il ne détecte pas colour comme une propriété en excès :

Lorsque les types conditionnels arrivent (#21316), vous pouvez effectuer les opérations suivantes pour exiger des types exacts comme paramètres de fonction, même pour les littéraux d'objet « non frais » :

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'.

Bien sûr, si vous élargissez le type, cela ne fonctionnera pas, mais je ne pense pas que vous puissiez vraiment faire quoi que ce soit à ce sujet :

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

Les pensées?

On dirait que des progrès sont en cours sur les paramètres de fonction. Quelqu'un a-t-il trouvé un moyen d'appliquer des types exacts pour une valeur de retour de fonction ?

@jezzgoodwin pas vraiment. Voir #241 qui est la cause première des retours de fonction qui ne sont pas correctement vérifiés pour des propriétés supplémentaires

Encore un cas d'utilisation. Je viens presque de rencontrer un bogue à cause de la situation suivante qui n'est pas signalée comme une erreur :

interface A {
    field: string;
}

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

type AorB = A | B;

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

( Aire de jeux )

La solution évidente pour cela pourrait être:

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

J'ai vu une solution de contournement proposée dans #16679 mais dans mon cas, le type est AorBorC (peut croître) et chaque objet a plusieurs propriétés, il est donc plutôt difficile de calculer manuellement un ensemble de propriétés fieldX?:never pour chaque genre.

@michalstocki N'est-ce pas #20863 ? Vous voulez que la vérification des biens excédentaires sur les unions soit plus stricte.

Quoi qu'il en soit, en l'absence de types exacts et de vérification de propriétés excessives strictes sur les unions, vous pouvez faire ces propriétés fieldX?:never programmation au lieu de manuellement en utilisant des types conditionnels :

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

Et puis définissez votre union comme

type AorB = ExclusifyUnion<A | B>;

qui s'étend jusqu'à

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

automatiquement. Cela fonctionne aussi pour n'importe quel AorBorC .

Voir également https://github.com/Microsoft/TypeScript/issues/14094#issuecomment -373780463 pour l'exclusivité ou la mise en œuvre

@jcalz Le type avancé ExclusifyUnion n'est pas très sûr :

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

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

Les champs de fields sont tous non facultatifs.

Je ne pense pas que cela ait beaucoup à voir avec les types Exact, mais avec ce qui se passe lorsque vous diffusez puis déstructurez un objet de type union . Toute union finira par s'aplatir en un seul type de type intersection, car elle sépare un objet en propriétés individuelles et les rejoint ensuite ; toute corrélation ou contrainte entre les constituants de chaque union sera perdue. Je ne sais pas comment l'éviter... s'il s'agit d'un bogue, cela pourrait être un problème distinct.

Évidemment, les choses se comporteront mieux si vous tapez guarding avant la déstructuration :

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
  }
}

Non pas que cela "résolve" le problème que vous voyez, mais c'est ainsi que je m'attendrais à ce que quelqu'un agisse avec un syndicat contraint.

Peut-être que https://github.com/Microsoft/TypeScript/pull/24897 résout le problème de propagation

Je suis peut-être en retard à la fête, mais voici comment vous pouvez au moins vous assurer que vos types correspondent exactement :

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

j'aimerais pouvoir faire ça :

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 fonctionnalité décrite dans ce numéro aiderait-elle dans le cas où une interface vide/indexée correspond à des types de type objet, comme des fonctions ou des classes ?

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!

L'interface MyType définit uniquement une signature d'index et est utilisée comme type du seul paramètre de la fonction test . Paramètre passé à la fonction de type :

  • Littéral d'objet {} , réussit. Comportement prévisible.
  • La constante numérique 1 ne passe pas. Comportement attendu (_Argument de type '1' n'est pas assignable au paramètre de type 'MyType'._)
  • Le littéral de chaîne '' ne passe pas. Comportement attendu (_`Argument de type '""' n'est pas assignable au paramètre de type 'MyType'._)
  • Déclaration de la fonction flèche () => {} : Passes. Comportement non attendu. Passe probablement parce que les fonctions sont des objets ?
  • Méthode de classe console.log Laissez-passer. Comportement non attendu. Similaire à la fonction flèche.
  • Laissez-passer console classe. Comportement non attendu. Probablement parce que les classes sont des objets ?

Le but est de n'autoriser que les variables qui correspondent exactement à l'interface MyType en étant déjà de ce type (et non converties implicitement). TypeScript semble effectuer beaucoup de conversions implicites basées sur des signatures, ce qui pourrait ne pas être pris en charge.

Désolé si c'est hors sujet. Jusqu'à présent, ce problème est le plus proche du problème que j'ai expliqué ci-dessus.

@Janne252 Cette proposition pourrait vous aider indirectement. En supposant que vous ayez essayé l'évident Exact<{[key: string]: any}> , voici pourquoi cela fonctionnerait :

  • Les littéraux d'objet passent comme prévu, comme ils le font déjà avec {[key: string]: any} .
  • Les constantes numériques échouent comme prévu, car les littéraux ne peuvent pas être affectés à {[key: string]: any} .
  • Les littéraux de chaîne échouent comme prévu, car ils ne sont pas attribuables à {[key: string]: any} .
  • Les fonctions et les constructeurs de classes échouent à cause de leur signature call (ce n'est pas une propriété de chaîne).
  • L'objet console passe parce que c'est juste ça, un objet (pas une classe). JS ne fait aucune séparation entre les objets et les dictionnaires clé/valeur, et TS n'est pas différent ici en dehors du typage polymorphe de ligne ajouté. De plus, TS ne prend pas en charge les types dépendants de la valeur, et typeof est simplement du sucre pour ajouter quelques paramètres supplémentaires et/ou alias de type - ce n'est pas aussi magique qu'il y paraît.

@blakeembrey @michalstocki @aleksey-bykov
C'est ma façon de faire des types exacts :

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; }>'.

Cela fonctionne pour les paramètres de fonctions, les retours et même pour les affectations.
const foo: Foo = fooGoo; // erreur
Pas de surcharge d'exécution. Le seul problème est que chaque fois que vous créez un nouvel objet exact, vous devez le convertir par rapport à son type, mais ce n'est pas vraiment grave.

Je pense que l'exemple d'origine a le bon comportement : je m'attends interface ce que type ce que les parfois ). Voici un exemple de comportement surprenant lors de l'écriture d'un type MappedOmit :
https://gist.github.com/donabrams/b849927f5a0160081db913e3d52cc7b3

Le type MappedOmit dans l'exemple fonctionne uniquement comme prévu pour les unions discriminées. Pour les unions non discriminées, Typescript 3.2 passe quand n'importe quelle intersection des types dans l'union est passée.

Les solutions de contournement ci-dessus en utilisant as TypeX ou as any pour caster ont pour effet secondaire de masquer les erreurs de construction !. Nous voulons que notre vérificateur de type nous aide également à détecter les erreurs de construction ! De plus, il y a plusieurs choses que nous pouvons générer statiquement à partir de types bien définis. Des solutions de contournement comme celles ci-dessus (ou les solutions de contournement de type nominal décrites ici : https://gist.github.com/donabrams/74075e89d10db446005abe7b1e7d9481) empêchent ces générateurs de fonctionner (bien que nous puissions filtrer les champs _ tête, c'est une convention douloureuse c'est absolument évitable).

@aleksey-bykov fyi, je pense que votre implémentation est à 99% du chemin, cela a fonctionné pour moi:

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, s'il vous plaît laissez les fleurs ici, les cadeaux vont dans ce bac

Une petite amélioration qui peut être faite ici :
En utilisant la définition suivante de Exact crée effectivement une soustraction de B de A comme A & never types sur tous les B clés uniques de

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

Enfin, je voulais pouvoir le faire sans avoir à ajouter l'utilisation explicite du modèle du deuxième argument de modèle B . J'ai pu faire fonctionner cela en enveloppant avec une méthode - pas idéale car elle affecte le temps d'exécution mais c'est utile si vous en avez vraiment vraiment besoin :

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

Exemple d'utilisation :

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 Il semble étrange de laisser B dans Exact<A, B> facultatif dans votre exemple, surtout si cela est requis pour une validation correcte. Sinon, ça m'a l'air très bien. Il semble cependant mieux nommé Equal .

@drabinowitz Votre type Exact ne représente pas réellement ce qui a été proposé ici et devrait probablement être renommé en quelque chose comme AreExact . Je veux dire, tu ne peux pas faire ça avec ton type :

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

Cependant, votre type est pratique pour implémenter le type de paramètre exact !

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 Cela ne créera pas de fonction d'exécution +1 comme dans la solution de @danielnmsft et est bien sûr beaucoup plus flexible.

UPD2 Je viens de me rendre compte que Daniel fabriquait en fait le même type Exact que @drabinowitz , mais plus compact et probablement meilleur. J'ai aussi réalisé que j'avais fait la même chose que Daniel avait fait. Mais je laisserai mon commentaire au cas où quelqu'un le trouverait utile.

Cette définition de AreSame / Exact ne semble pas fonctionner pour le type union.
Exemple : Exact<'a' | 'b', 'a' | 'b'> donne never .
Cela peut apparemment être corrigé en définissant type AreSame<A, B> = A|B extends A&B ? true : false;

@nerumo a définitivement trouvé cela pour le même type de fonction de

Quelques options supplémentaires parmi celles que vous aviez :

1 Vous pouvez définir le type de retour comme étant le même que le type d'entrée avec typeof . Plus utile si c'est un type très compliqué. Pour moi, quand je regarde cela, il est plus explicitement évident que l'intention est d'empêcher les propriétés supplémentaires.

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 Pour les réducteurs, au lieu d'une variable temporaire, attribuez-la à elle-même avant de retourner :

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 vous voulez vraiment une variable temporaire, ne lui donnez pas de type explicite, utilisez à nouveau typeof state

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 votre réducteur ne contient pas ...state vous pouvez utiliser Partial<typeof state> pour le type :

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;
}

Je pense que toute cette conversation (et je viens de lire tout le fil) a raté le nœud du problème pour la plupart des gens et c'est que pour éviter les erreurs, tout ce que nous voulons, c'est une assertion de type pour éviter d'interdire un type "plus large":

C'est ce que les gens peuvent essayer en premier, ce qui n'interdit pas 'fullName' :

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

En effet , <Dog> cat vous dit le compilateur - oui je sais ce que je fais, est un Dog ! Vous ne demandez pas la permission.

Donc, ce qui me serait le plus utile, c'est une version plus stricte de <Dog> cat qui empêcherait les propriétés superflues :

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

L'ensemble du type Exact<T> a de nombreuses conséquences (c'est un long fil !). Cela me rappelle tout le débat sur les «exceptions vérifiées» où c'est quelque chose que vous pensez vouloir mais il s'avère qu'il y a de nombreux problèmes (comme soudainement cinq minutes plus tard, vouloir un Unexact<T> ).

D'un autre côté, <strict T> agirait plutôt comme une barrière pour empêcher les types « impossibles » de « passer ». Il s'agit essentiellement d'un filtre de type qui traverse le type (comme cela a été fait ci-dessus avec les fonctions d'exécution).

Cependant, il serait facile pour les nouveaux arrivants de supposer que cela empêchait les « mauvaises données » de passer dans les cas où il leur serait impossible de le faire.

Donc si je devais faire une proposition de syntaxe ce serait celle-ci :

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

Retour à l'OP : en théorie [1] avec des types niés, vous pourriez écrire type Exact<T> = T & not Record<not keyof T, any> . Ensuite, un Exact<{x: string}> interdirait à tout type avec des clés autres que x de lui être assigné. Je ne sais pas si cela suffit à satisfaire ce qui est demandé par tout le monde ici, mais cela semble parfaitement correspondre au PO.

[1] Je dis en théorie parce que cela repose également sur de meilleures signatures d'index

Curieux de savoir si j'ai le problème décrit ici. J'ai un code comme :

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

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

const groups = {} as Groups

alors cela me permet de définir des propriétés inconnues, ce que je ne veux pas :

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

Le paramètre anything fonctionne toujours et le type de valeur clé est any . J'espérais que ce serait une erreur.

Est-ce ce qui sera résolu par ce problème ?

Il s'avère que j'aurais dû faire

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

Notez l'utilisation supplémentaire de typeof .

Le plugin Atom atom-typescript s'efforçait de ne pas échouer, et a fini par planter. Lorsque j'ai ajouté typeof , les choses sont revenues à la normale et les accessoires inconnus n'étaient plus autorisés, ce à quoi je m'attendais.

En d'autres termes, lorsque je n'utilisais pas typeof , atom-typescript essayait de comprendre le type à d'autres endroits du code où j'utilisais les objets de type Groups , et cela me permettait d'ajouter des accessoires inconnus et me montrait un indice de type de any pour eux.

Je ne pense donc pas avoir le problème de ce fil.

Une autre complication pourrait être de savoir comment gérer les propriétés facultatives.

Si vous avez un type qui a des propriétés facultatives, que signifierait Exact<T> pour ces propriétés :

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

Est-ce que Exact<T> signifie que chaque propriété facultative doit être définie ? Comment le spécifieriez-vous ? Pas "undefined" ou "null" car cela a un effet d'exécution.

Cela nécessite-t-il maintenant une nouvelle façon de spécifier un « paramètre facultatif obligatoire » ?

Par exemple, avec quoi devons-nous affecter amountCharged dans l'exemple de code suivant pour qu'il satisfasse à la « exactitude » du type ? Nous ne sommes pas très "exacts" si nous n'appliquons pas cette propriété pour qu'elle soit au moins "reconnue" d'une manière ou d'une autre. Est-ce <never> ? Cela ne peut pas être undefined ou null .

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

Vous pensez peut-être - c'est toujours facultatif, et c'est maintenant exactement facultatif, ce qui se traduit simplement par facultatif . Et certainement au moment de l' Exact<T> en collant un point d'interrogation.

Peut-être n'est-ce que lors de l'attribution d'une valeur entre deux types que cette vérification doit être effectuée ? (Pour imposer qu'ils incluent tous les deux amountCharged?: number )

Introduisons ici un nouveau type pour les données d'entrée d'une boîte de dialogue :

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

Essayons donc ceci :

// 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

Je m'attendrais à ce que cela échoue bien sûr à cause de la faute de frappe - même si cette propriété est facultative dans les deux.

Alors je suis revenu à « Pourquoi voulons-nous cela en premier lieu ? » .
Je pense que ce serait pour ces raisons (ou un sous-ensemble selon la situation):

  • Évitez les fautes de frappe dans les noms de propriété
  • Si nous ajoutons une propriété à un « composant », nous voulons nous assurer que tout ce qui l'utilise doit également ajouter cette propriété
  • Si nous supprimons une propriété d'un « composant », nous devons la supprimer partout.
  • Assurez-vous que nous ne fournissons pas de propriétés supplémentaires inutilement (peut-être que nous l'envoyons à une API et que nous voulons garder la charge utile légère)

Si les « propriétés facultatives exactes » ne sont pas gérées correctement, certains de ces avantages sont brisés ou très confus !

De plus, dans l'exemple ci-dessus, nous avons simplement "enculé" Exact pour essayer d'éviter les fautes de frappe, mais nous n'avons réussi qu'à faire un énorme gâchis ! Et il est maintenant encore plus cassant que jamais.

Je pense que ce dont j'ai souvent besoin n'est pas du tout un type Exact<T> , c'est l'un de ces deux :

NothingMoreThan<T> ou
NothingLessThan<T>

'requis facultatif' est maintenant une chose. Le premier ne permet à rien de plus d'être défini par le RHS de l'affectation, et le second s'assure que tout (y compris les propriétés facultatives) est spécifié sur le RHS d'une affectation.

NothingMoreThan serait utile pour les charges utiles envoyées sur le fil, ou JSON.stringify() et si vous deviez obtenir une erreur parce que vous aviez trop de propriétés sur RHS, vous deviez écrire le code d'exécution pour sélectionner uniquement les propriétés nécessaires. Et c'est la bonne solution - car c'est ainsi que Javascript fonctionne.

NothingLessThan est un peu ce que nous avons déjà en dactylographié - pour toutes les affectations normales - sauf qu'il faudrait considérer les propriétés optionnelles (optional?: number) .

Je ne m'attends pas à ce que ces noms fassent une traction, mais je pense que le concept est plus clair et plus granulaire que Exact<T> ...

Ensuite, peut-être (si nous en avons vraiment besoin) :

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

ou serait-ce :

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

Ce message est le résultat d'un réel problème que j'ai aujourd'hui où j'ai un « type de données de boîte de dialogue » qui contient des propriétés facultatives et je veux m'assurer que ce qui vient du serveur lui est attribuable.

Note finale : NothingLessThan / NothingMoreThan ont une "sensation" similaire à certains des commentaires ci-dessus où le type A est étendu du type B, ou B est étendu de A. La limitation est qu'ils ne traiterait pas des propriétés facultatives (du moins, je ne pense pas qu'ils le pourraient aujourd'hui).

@simeyla Vous pourriez simplement vous en tirer avec la variante "rien de plus que".

  • "Rien de moins que" n'est que des types normaux. TS le fait implicitement, et chaque type est traité comme équivalent à un for all T extends X: T .
  • "Rien de plus que" est fondamentalement le contraire : c'est un for all T super X: T implicite

Un moyen d'en choisir un ou les deux explicitement serait suffisant. Comme effet secondaire, vous pouvez spécifier le T super C de Java comme votre T extends NothingMoreThan<C> proposé. Je suis donc assez convaincu que c'est probablement mieux que les types exacts standard.

Je pense que cela devrait être de la syntaxe cependant. Peut être ça?

  • extends T - L'union de tous les types assignables à T, c'est-à-dire équivalente à tout simplement T .
  • super T - L'union de tous les types T est assignable.
  • extends super T , super extends T - L'union de tous les types équivalents à T. Cela sort de la grille, puisque seul le type peut être à la fois assignable et assigné à lui-même.
  • type Exact<T> = extends super T - Sugar intégré pour le cas courant ci-dessus, pour faciliter la lisibilité.
  • Étant donné que cela ne fait que basculer l'assignabilité, vous pouvez toujours avoir des choses comme des unions qui sont des types exacts ou super.

Cela permet également d'implémenter #14094 dans l'espace utilisateur en faisant simplement chaque variante Exact<T> , comme Exact<{a: number}> | Exact<{b: number}> .


Je me demande si cela rend également possible les types niés dans l'espace utilisateur. Je crois que oui, mais je devrais d'abord faire une arithmétique de type compliquée pour le confirmer, et ce n'est pas exactement une chose évidente à prouver.

Je me demande si cela rend également possible les types niés dans l'espace utilisateur, puisque (super T) | (étend T) est équivalent à inconnu. Je crois que oui, mais je devrais d'abord faire une arithmétique de type compliquée pour le confirmer, et ce n'est pas exactement une chose évidente à prouver.

Pour que (super T) | (extends T) === unknown conserve l'assignation, il faudrait une commande totale.

@jack-williams Bonne prise et corrigé (en supprimant la réclamation). Je me demandais pourquoi les choses ne fonctionnaient pas au début quand je jouais un peu.

@jack-williams

"Rien de moins que" n'est que des types normaux. TS le fait implicitement, et chaque type est traité comme équivalent

Oui et non. Mais surtout oui... ...mais seulement si vous êtes en mode strict !

J'ai donc eu beaucoup de situations où j'avais besoin qu'une propriété soit logiquement « facultative », mais je voulais que le compilateur me dise si je l'avais « oubliée » ou si je l'avais mal orthographié.

Eh bien, c'est exactement ce que vous obtenez avec lastName: string | undefined alors que j'avais surtout lastName?: string , et bien sûr sans le mode strict vous ne serez pas prévenu de toutes les divergences.

J'ai toujours connu le mode strict, et je ne peux pas pour la vie de moi trouver une bonne raison pour laquelle je ne l'ai pas activé jusqu'à hier - mais maintenant que je l'ai (et je patauge toujours dans des centaines de correctifs ) il est beaucoup plus facile d'obtenir le comportement que je voulais « out of the box ».

J'avais essayé toutes sortes de choses pour obtenir ce que je voulais - y compris jouer avec Required<A> extends Required<B> , et essayer de supprimer les indicateurs de propriété optionnels ? . Cela m'a envoyé dans un tout autre terrier de lapin - (et tout cela avant d'activer le mode strict ).

Le fait est que si vous essayez d'obtenir quelque chose de proche des types "exacts" aujourd'hui, vous devez commencer par activer le mode strict (ou toute combinaison de drapeaux donnant les bons contrôles). Et si j'avais besoin d'ajouter middleName: string | undefined plus tard, alors boum - je trouverais soudainement partout où j'avais besoin de « le considérer » :-)

PS. merci pour vos commentaires - a été très utile. Je me rends compte que j'ai vu BEAUCOUP de code qui n'utilise clairement pas le mode strict - et puis les gens se heurtent à des murs comme je l'ai fait. Je me demande ce qui peut être fait pour encourager davantage son utilisation?

@simeyla Je pense que vos commentaires et remerciements doivent être adressés à @isiahmeadows !

J'ai pensé que j'écrirais mes expériences avec les types Exact après avoir implémenté un prototype de base. Mon opinion générale est que l'équipe a été juste avec son évaluation :

Notre diagnostic optimiste est qu'il s'agit, en dehors des relativement peu d'API vraiment fermées, d'une solution au problème XY.

Je ne pense pas que le coût de l'introduction d'un autre type d'objet soit remboursé en attrapant plus d'erreurs ou en activant de nouvelles relations de type. En fin de compte, les types exacts m'ont laissé _dire_ plus, mais ils ne m'ont pas laissé _faire_ plus.

Examen de certains des cas d'utilisation potentiels de types exacts :

Taper fort pour keys et for ... in .

Avoir des types plus précis lors de l'énumération des clés semble attrayant, mais en pratique, je ne me suis jamais retrouvé à énumérer des clés pour des choses qui étaient conceptuellement exactes. Si vous connaissez précisément les clés, pourquoi ne pas simplement les adresser directement ?

Durcissement facultatif de l'élargissement de la propriété.

La règle d'assignation { ... } <: { ...; x?: T } n'est pas valable car le type de gauche peut inclure une propriété x incompatible qui a été aliasée. Lors de l'affectation à partir d'un type exact, cette règle devient valable. En pratique, je n'utilise jamais cette règle ; il semble plus adapté aux systèmes hérités qui n'auraient pas de types exacts pour commencer.

Réagir et HOC

J'avais épinglé mon dernier espoir sur les types exacts améliorant le passage des accessoires et la simplification des types de propagation. La réalité est que les types exacts sont l'antithèse du polymorphisme borné et fondamentalement non compositionnels.

Un générique délimité vous permet de spécifier les accessoires qui vous intéressent et de passer le reste à travers. Dès que la limite devient exacte, vous perdez complètement le sous-typage de largeur et le générique devient nettement moins utile. Un autre problème est que l'un des principaux outils de composition dans TypeScript est l'intersection, mais les types d'intersection sont incompatibles avec les types exacts. Tout type d'intersection non trivial avec un composant exact va être vide : _les types exacts ne composent pas_. Pour les réactions et les accessoires, vous voulez probablement des types de lignes et un polymorphisme de lignes, mais c'est pour un autre jour.

Presque tous les bogues intéressants qui pourraient être résolus par des types exacts sont résolus par une vérification excessive des propriétés ; Le plus gros problème est que la vérification excessive des propriétés ne fonctionne pas pour les unions sans propriété discriminante ; résoudre ce problème et presque tous les problèmes intéressants pertinents pour les types exacts disparaissent, OMI.

@jack-williams Je suis d'accord qu'il n'est généralement pas très utile d'avoir des types exacts. Le concept de vérification des propriétés en excès est en fait couvert par ma proposition d'opérateur super T , juste indirectement parce que l'union de tous les types auxquels T est assignable n'inclut notamment

Je ne suis pas très favorable à cela personnellement, à part peut-être un T super U *, car le seul cas d'utilisation que j'ai jamais rencontré pour la vérification excessive des propriétés concernait des serveurs cassés, quelque chose que vous pouvez généralement contourner en en utilisant une fonction wrapper pour générer les requêtes manuellement et supprimer les déchets en excès. Tous les autres problèmes que j'ai trouvés signalés dans ce fil jusqu'à présent pourraient être résolus simplement en utilisant une simple union discriminée.

* Ce serait essentiellement T extends super U utilisant ma proposition - les limites inférieures sont parfois utiles pour contraindre les types génériques contravariants, et les solutions de contournement finissent généralement par introduire de nombreux types de passe-partout supplémentaires dans mon expérience.

@isiahmeadows Je suis certainement d'accord que les types à bornes inférieures peuvent être utiles, et si vous pouvez en tirer des types exacts, alors c'est une victoire pour ceux qui veulent les utiliser. Je suppose que je devrais ajouter une mise en garde à mon message, à savoir : j'aborde principalement le concept d'ajout d'un nouvel opérateur spécifiquement pour les types d'objets exacts.

@jack-williams Je pense que vous avez manqué ma nuance selon laquelle je faisais principalement référence aux types exacts et à la partie connexe de la vérification des propriétés en excès. Le peu sur les types à bornes inférieures était une note de bas de page pour une raison - c'était une digression qui n'est que tangentiellement liée.

J'ai réussi à écrire une implémentation pour cela qui fonctionnera pour les arguments de fonction qui nécessitent divers degrés d'exactitude :

// 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


Le type exact d'affectation de variable dont j'ai réalisé qu'il ne faisait rien...

// 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

Voir

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

J'ai l'impression d'avoir un cas d'utilisation pour cela lors de l'encapsulation des composants React, où je dois "passer à travers" les accessoires: https://github.com/Microsoft/TypeScript/issues/29883. @jack-williams Avez-vous une idée à ce sujet ?

@OliverJAsh Cela semble pertinent, mais je dois admettre que je ne connais pas aussi bien React que la plupart. Je suppose qu'il serait utile de comprendre comment les types exacts peuvent précisément aider ici.

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}
    />
);

Veuillez me corriger à tout moment où je dis quelque chose de mal.

Je suppose que le début serait de spécifier MyComponent pour accepter un type exact ?

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

Dans ce cas, nous obtiendrions une erreur, mais comment corrigez-vous l'erreur ? Je suppose ici que les composants wrapper n'ont pas simplement le même type d'accessoire jusqu'au bout, et qu'à un moment donné, vous devez vraiment extraire dynamiquement un sous-ensemble d'accessoires. Est-ce une hypothèse raisonnable ?

Si MyWrapperComponent props est également exact, alors je pense qu'il suffirait de faire une liaison déstructurante. Dans le cas générique, cela nécessiterait un type Omit sur un type exact, et je ne connais vraiment pas la sémantique là-bas. Je suppose que cela pourrait fonctionner comme un type mappé homomorphe et conserver l'exactitude, mais je pense que cela nécessiterait plus de réflexion.

Si MyWrapperComponent n'est pas exact, cela nécessitera une vérification à l'exécution pour prouver l'exactitude du nouveau type, ce qui ne peut être fait qu'en sélectionnant explicitement les propriétés souhaitées (qui ne sont pas mises à l'échelle comme vous le dites dans votre PO). Je ne sais pas combien vous gagnez dans ce cas.

Les choses que je n'ai pas couvertes parce que je ne sais pas à quel point elles sont probables sont le cas générique, où props est un type générique, et où vous devez combiner des accessoires comme { ...props1, ...props2 } . Est-ce courant ?

@Kotarski L' avez-vous publié par hasard dans le registre NPM ?

@gitowiec

@Kotarski L' avez-vous publié par hasard dans le registre NPM ?

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

J'ai ce cas d'utilisation :

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 Cela n'a pas besoin de types exacts; qui a juste besoin d'un correctif pour la vérification excessive des propriétés : #13813.

@ryami333 Si vous êtes prêt à utiliser un type supplémentaire, j'ai un type qui fera ce que vous voulez, à savoir forcer une version plus stricte des unions :

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 Fascinant. C'est curieux pour moi pourquoi

type KeyofV1<T extends object> = keyof T

produit un résultat différent de

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

Quelqu'un pourrait-il m'expliquer cela?

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 obtient les clés communes de l'union, V2 obtient les clés de chaque membre de l'union et les syndicats le résultat.

@weswigham Y a-t-il une raison pour laquelle ils devraient renvoyer des résultats différents ?

Oui? Comme je l'ai dit - V1 obtient les _clés communes_ à chaque membre du syndicat, car l'argument de keyof finit par être keyof (AB | ABCD) , qui est juste "A" | "B" , tandis que la version au sein du conditionnel ne reçoit qu'un membre du syndicat à la fois, grâce à la distribution conditionnelle sur son entrée, il s'agit donc essentiellement de keyof AB | keyof ABCD .

@weswigham Donc, le conditionnel l'évalue plus comme ça, comme via une boucle implicite?

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

Lorsque je lis ce code, je m'attendrais normalement à ce que le (AB | ABCD) extends object fonctionne comme une seule unité, vérifiant que (AB | ABCD) est attribuable à object , puis il renvoie keyof (AB | ABCD) tant qu'unité, 'a' | 'b' . Le mappage implicite me semble vraiment étrange.

@isiahmeadows Vous pouvez regarder les types conditionnels distributifs comme foreach pour les unions. Ils appliquent le type conditionnel à chaque membre de l'union à tour de rôle et le résultat est l'union de chaque résultat partiel.

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

Mais seulement si le type conditionnel distribue, et il ne distribue que si le type testé est un paramètre de type nu. Donc:

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

Merci les gars, je pense avoir compris. Je l'ai réarrangé pour ma compréhension; Je pense que le NegativeUncommonKeys est également utile en soi. Le voici au cas où cela serait utile à quelqu'un d'autre également.

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;

Je comprends aussi pourquoi T et TAll sont tous les deux là. L'"effet de boucle", où T est testé et nu, signifie que chaque élément de l'union pour T est appliqué alors que le TAll non testé contient l'union originale et complète de tous les éléments.

C'est le segment du

@weswigham Ouais .. sauf que je pense que cette section se lit comme si elle avait été écrite par un ingénieur compilateur pour un autre ingénieur compilateur.

Les types conditionnels dans lesquels le type vérifié est un paramètre de type nu sont appelés types conditionnels distributifs.

Quels sont les paramètres de type nu ? (et pourquoi ne mettent-ils pas des vêtements 😄)

c'est-à-dire que T fait référence aux constituants individuels après que le type conditionnel est distribué sur le type union)

Pas plus tard qu'hier, j'ai eu une discussion sur ce que signifie cette phrase particulière et pourquoi l'accent était mis sur le mot « après ».

Je pense que la documentation est écrite en supposant des connaissances préalables et une terminologie que les utilisateurs peuvent ne pas toujours avoir.

La section du manuel a du sens pour moi et elle l'explique beaucoup mieux, mais je suis toujours sceptique quant au choix de conception. Cela n'a tout simplement pas de sens logique pour moi comment ce comportement suivrait naturellement d'une perspective de théorie des ensembles et de la théorie des types. Cela semble juste un peu trop hackish.

découlent naturellement d'une perspective de théorie des ensembles et de la théorie des types

Prenez chaque élément d'un ensemble et partitionnez-le selon un prédicat.

C'est une opération distributive !

Prenez chaque élément d'un ensemble et partitionnez-le selon un prédicat.

Bien que cela n'ait de sens que lorsque vous parlez d'ensembles d'ensembles (c'est-à-dire d'un type d'union) qui commence à ressembler beaucoup plus à la théorie des catégories.

@RyanCavanaugh D'accord, permettez-moi de clarifier : je lis intuitivement T extends U ? F<T> : G<T> comme T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T) , la comparaison n'étant pas faite par morceaux, mais comme une étape complète. C'est nettement différent de "l'union de pour tous les {if t ∈ U then F({t}) else G({t}) | t ∈ T} , qui est actuellement la sémantique.

(Pardon si ma syntaxe est un peu décalée - mes connaissances en théorie des types sont entièrement autodidactes, donc je sais que je ne connais pas tous les formalismes syntaxiques.)

Quelle opération est la plus intuitive est sujette à un débat infini, mais avec les règles actuelles, il est facile de rendre un type distributif non distributif avec [T] extends [C] . Si la valeur par défaut n'était pas distributive, vous auriez besoin d'une nouvelle incantation à un niveau différent pour provoquer la distributivité. C'est aussi une question distincte à partir de laquelle le comportement est le plus souvent préféré ; IME Je ne veux presque jamais d'un type non-distributeur.

Oui, il n'y a pas de fondement théorique solide pour la distribution car c'est une opération syntaxique.

La réalité est qu'il est très utile et essayer de le coder d'une autre manière serait douloureux.

Dans l'état actuel des choses, je vais aller de l'avant et m'arrêter avant de pousser la conversation trop loin du sujet.

il y a déjà tellement de problèmes de distribution, pourquoi ne devons-nous pas faire face à la nécessité d'une nouvelle syntaxe ?

30572

Voici un exemple de problème :

Je souhaite spécifier que le point de terminaison/le service de l'API de mes utilisateurs ne doit PAS renvoyer de propriétés supplémentaires (comme, par exemple, un mot de passe) autres que celles spécifiées dans l'interface de service. Si je renvoie accidentellement un objet avec des propriétés supplémentaires, je veux une erreur de compilation, que l'objet résultat ait été produit par un objet littéral ou non.

Une vérification à l'exécution de chaque objet renvoyé peut être coûteuse, en particulier pour les tableaux.

Une vérification excessive des propriétés n'aide pas dans ce cas. Honnêtement, je pense que c'est une solution bancale à un tour. En théorie, cela aurait dû fournir une expérience de type "ça marche juste" - en pratique, c'est aussi une source de confusion Les types d'objets exacts auraient dû être implémentés à la place, ils auraient bien couvert les deux cas d'utilisation.

@babakness Votre type NoExcessiveProps est un no-op. Je pense qu'ils veulent dire quelque chose comme ça :

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?
    }
}

En tant qu'auteur du type d'API, vous souhaitez imposer que username renvoie simplement le nom d'utilisateur, mais n'importe quel implémenteur peut contourner cela car les types d'objets n'ont aucune restriction de largeur. Cela ne peut être appliqué qu'à l'initialisation d'un littéral, ce que l'implémenteur peut ou non faire. Cependant, je déconseille fortement à quiconque d'essayer d'utiliser des types exacts comme sécurité basée sur le langage.

@spion

Une vérification excessive des propriétés n'aide pas dans ce cas. Honnêtement, je pense que c'est une solution bancale à un tour. En théorie, ils auraient dû fournir une expérience du genre "ça marche"

EPC est un choix de conception raisonnablement raisonnable et léger qui couvre un grand nombre de problèmes. La réalité est que les types Exact ne « font pas que fonctionner ». Pour mettre en œuvre d'une manière solide qui prend en charge l'extensibilité, il faut un système de type complètement différent.

@jack-williams Bien sûr, il existerait également d'autres moyens de vérifier la présence (contrôles d'exécution où les performances ne sont pas un problème, tests, etc.), mais un temps de compilation supplémentaire est inestimable pour un retour rapide.

De plus, je ne voulais pas dire que les types exacts "fonctionnent simplement". Je voulais dire qu'EPC était censé "fonctionner", mais en pratique, c'est juste limité, déroutant et dangereux. Principalement parce que si vous essayez de l'utiliser "délibérément", vous finissez généralement par vous tirer une balle dans le pied.

edit: Oui, j'ai modifié pour remplacer "ils" par "il" car je me suis rendu compte que c'était déroutant.

@spion

De plus, je ne voulais pas dire que les types exacts "fonctionnent simplement". Je voulais dire qu'EPC était censé "fonctionner", mais en pratique, c'est juste limité, déroutant et dangereux. Principalement parce que si vous essayez de l'utiliser "délibérément", vous finissez généralement par vous tirer une balle dans le pied.

Mon erreur. Lisez le commentaire original comme

En théorie, ils auraient dû fournir une expérience de type "ça marche juste" [qui aurait été des types exacts au lieu d'EPC]

commentaire dans [] étant ma lecture.

La déclaration révisée :

En théorie, cela aurait dû fournir une expérience de type "ça marche"

est beaucoup plus clair. Désolé pour ma mauvaise interprétation !

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 
}


Le résultat pour getUser2 est surprenant, il semble incohérent et devrait produire une erreur de compilation. Quelle est la perspicacité sur pourquoi il ne le fait pas ?

@babakness Votre NoExcessiveProps revient juste à T (enfin un type avec les mêmes clés que T ). Dans [K in keyof O]: K extends keyof O ? O[K] : never , K sera toujours une clé de O puisque vous mappez sur keyof O . Vos erreurs d'exemple const car il déclenche EPC comme il l'aurait fait si vous l'aviez tapé sous la forme {username: string} .

Si cela ne vous dérange pas d'appeler une fonction supplémentaire, nous pouvons capturer le type réel de l'objet transmis et effectuer une forme personnalisée de vérification des propriétés en excès. (Je me rends compte que le but est d'attraper automatiquement ce type d'erreur, donc cela pourrait avoir une valeur limitée):

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... d'accord... bon point ! J'essaie donc de comprendre votre fonction checked . je suis particulièrement perplexe

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
}

L'affectation bar dans getUser3 échoue. L'erreur semble être à foo
image

Détails de l'erreur

image

Le type pour bar ici est {} , ce qui semble être parce que sur checked

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

E n'est attribué nulle part. Pourtant, si nous remplaçons typeof E par typeof {} , cela ne fonctionne pas.

Quel est le type de E ? Y a-t-il une sorte de chose sensible au contexte qui se passe?

@babakness S'il n'y a pas d'autre endroit pour déduire un paramètre de type, typescript le déduira du type de retour. Ainsi, lorsque nous attribuons le résultat de checked au retour de getUser* , E sera le type de retour de la fonction, et T sera le type réel de la valeur que vous souhaitez renvoyer. S'il n'y a pas de place pour en déduire E valeur par défaut sera simplement {} et vous obtiendrez donc toujours une erreur.

La raison pour laquelle je l'ai fait comme ça était d'éviter tout paramètre de type explicite, vous pouvez en créer une version plus explicite :

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
}

Remarque : L'approche de la fonction curry est nécessaire car nous n'avons pas encore d'inférence d'argument partielle (https://github.com/Microsoft/TypeScript/pull/26349), nous ne pouvons donc pas spécifier certains paramètres de type et en faire inférer d'autres dans le même appel. Pour contourner ce problème, nous spécifions E dans le premier appel et laissons T être déduit dans le deuxième appel. Vous pouvez également mettre en cache la fonction cache pour un type spécifique et utiliser la version mise en cache

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 il s'agit d'une règle WIP / sketch tslint qui résout le problème spécifique de ne pas renvoyer accidentellement des propriétés supplémentaires à partir de méthodes "exposées".

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

Pour le cas d'utilisation de propagation - voir https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -300382189 - un linter pourrait-il détecter un modèle comme celui-ci et avertir qu'il n'est pas sécurisé ?

Copie de l'exemple de code à partir du commentaire ci-dessus :

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

J'aimerais beaucoup que cela se produise. Nous utilisons des définitions TypeScript générées pour les points de terminaison GraphQL et c'est un problème que TypeScript ne génère pas d'erreur lorsque je passe un objet avec plus de champs que nécessaire à une requête car GraphQL ne parviendra pas à exécuter une telle requête au moment de l'exécution.

dans quelle mesure cela est-il désormais résolu avec la mise à jour 3.5.1 avec une meilleure vérification des propriétés supplémentaires pendant l'affectation ? nous avons eu un tas de problèmes connus signalés comme des erreurs comme nous le voulions après la mise à niveau vers 3.5.1

si vous avez un problème et que vous pensez que les types exacts sont la bonne solution, veuillez décrire le problème d'origine ici

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

En voici une impliquant les références React : https://github.com/microsoft/TypeScript/issues/31798

/cc @RyanCavanaugh

Un cas d'utilisation pour moi est

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;
  };

Ce n'est pas valable si nous n'utilisons pas de types exacts, car si object a des propriétés supplémentaires, il n'est pas prudent d'appeler mapper sur ces clés et valeurs supplémentaires.

La vraie motivation ici est que je veux avoir les valeurs d'une énumération quelque part que je puisse réutiliser dans le code :

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

this.props.callback a le type (keyof typeof choices) => void .

Il s'agit donc vraiment du système de types capable de représenter le fait que j'ai une liste de clés dans le code land qui correspond exactement à un ensemble (par exemple, une union) de clés dans le type land, afin que nous puissions écrire des fonctions qui opèrent sur ce liste de clés et faire des assertions de type valides sur le résultat. Nous ne pouvons pas utiliser un objet ( choices dans mon exemple précédent) car pour autant que le système de types le sache, l'objet code-land pourrait avoir des propriétés supplémentaires au-delà du type d'objet utilisé. Nous ne pouvons pas utiliser un tableau ( ['choice0', 'choice1', 'choice2'] as const , car pour autant que le système de types le sache, le tableau peut ne pas contenir toutes les clés autorisées par le type de tableau.

Peut-être que exact ne devrait pas être un type, mais seulement un modificateur sur les entrées et/ou la sortie de la fonction ? Quelque chose comme le modificateur de variance du flux ( + / - )

Je veux compléter ce que @phaux vient de dire. La véritable utilité que j'ai pour Exact est que le compilateur garantisse la forme des fonctions. Lorsque j'ai un framework, je peux avoir besoin de l'un de ceux-ci : (T, S): AtMost<T> , (T, S): AtLeast<T> , ou (T, S): Exact<T> où le compilateur peut vérifier que les fonctions définies par l'utilisateur s'adapteront exactement.

Quelques exemples utiles :
AtMost est utile pour la configuration (donc nous n'ignorons pas les paramètres/fautes de frappe supplémentaires et nous échouons tôt).
AtLeast est idéal pour des choses comme les composants réactifs et les middleware où un utilisateur peut insérer tout ce qu'il veut en plus sur un objet.
Exact est utile pour la sérialisation/désérialisation (nous pouvons garantir que nous ne perdons pas de données et celles-ci sont isomorphes).

Cela aiderait-il à empêcher que cela se produise?

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

Nous avons vraiment besoin d'un moyen dans TS de dire T extends exactly { something: boolean; } ? xxx : yyy .

Ou sinon, quelque chose comme :

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

Y retournera toujours xxx .

Peut-être que le mot-clé const peut être utilisé ? par exemple T extends const { something: boolean }

@pleerock, cela peut être légèrement ambigu, car dans JavaScript / TypeScript, nous pouvons définir une variable comme const mais toujours ajouter / supprimer des propriétés d'objet. Je pense que le mot-clé exact est assez précis.

Je ne sais pas si c'est exactement lié, mais je m'attendrais à au moins deux erreurs dans ce cas :
terrain de jeux
Screen Shot 2019-08-08 at 10 15 34

@mityok Je pense que c'est lié. Je suppose que vous aimeriez faire quelque chose du genre :

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

Si le exact rend le type plus strict - alors il ne devrait pas être extensible avec une propriété supplémentaire, comme vous l'avez fait dans Dog .

en tirant parti des const ( as const ) et en utilisant des interfaces et des types avant, comme

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

serait vraiment verbeux d'avoir à affecter des variables const, mais éviterait beaucoup de cas limites lorsque vous passez un alias de type qui n'était pas exactement ce à quoi vous vous attendiez

J'ai trouvé une solution TypeScript pure pour le problème de Exact<T> qui, je crois, se comporte exactement comme ce qui a été demandé dans le post 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 raison ExactInner laquelle Exact est que le correctif #32824 n'est pas encore publié (mais déjà fusionné dans !32924 ).

Il n'est possible d'attribuer une valeur à la variable ou à l'argument de fonction de type Exact<T> , si l'expression de droite est également Exact<T> , où T est exactement de type identique dans les deux parties d'affectation.

Je n'ai pas obtenu la promotion automatique des valeurs dans les types exacts, c'est donc à cela que sert la fonction d'assistance exact() . Toute valeur peut être promue pour être de type exact, mais l'affectation ne réussira que si TypeScript peut prouver que les types sous-jacents des deux parties de l'expression ne sont pas seulement extensibles, mais exactement les mêmes.

Cela fonctionne en exploitant le fait que TypeScript utilise la vérification de relation extend pour déterminer si le type de main droite peut être attribué au type de main gauche - il ne le peut que si le type de main droite (source) _étend_ le type de main gauche (destination) .

Citant checker.ts ,

// Deux types conditionnels 'T1 étend U1 ? X1 : Y1' et 'T2 prolongent U2 ? X2 : Y2' sont liés si
// l'un de T1 et T2 est lié à l'autre, U1 et U2 sont de types identiques, X1 est lié à X2,
// et Y1 est lié à Y2.

ExactInner<T> generic utilise l'approche décrite, en substituant U1 et U2 par des types sous-jacents qui nécessitent des contrôles d'exactitude. Exact<T> ajoute une intersection avec le type sous-jacent simple, ce qui permet à TypeScript de relâcher le type exact lorsque sa variable cible ou son argument de fonction n'est pas un type exact.

Du point de vue du programmeur, Exact<T> se comporte comme s'il définissait un indicateur exact sur T , sans inspecter T ni le modifier, et sans créer de type indépendant.

Voici le lien du terrain de jeu et le lien essentiel .

Une amélioration future possible serait de permettre la promotion automatique des types non exacts en types exacts, supprimant complètement le besoin de la fonction exact() .

Un travail incroyable @toriningen!

Si quelqu'un est capable de trouver un moyen de faire fonctionner cela sans avoir à envelopper votre valeur dans un appel à exact ce serait parfait.

Je ne sais pas si c'est le bon problème, mais voici un exemple de quelque chose que j'aimerais travailler.

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 C'est différent. Cela demande une fonctionnalité pour les types qui n'admettent pas de propriétés supplémentaires, comme certains types exact {foo: number}{foo: 1, bar: 2} ne lui sont pas assignables. Cela demande simplement que des transformations de texte s'appliquent aux valeurs d'énumération, ce qui n'existe probablement pas.

Je ne sais pas si c'est la bonne question, mais [...]

D'après mon expérience en tant que mainteneur ailleurs, si vous avez un doute et que vous ne trouvez aucun problème existant clair, déposez un nouveau bogue et le pire des cas, il est fermé comme un dupe que vous n'avez pas trouvé. C'est à peu près le cas dans la plupart des grands projets JS open source. (La plupart d'entre nous, les plus gros responsables de la communauté JS, sont en fait des gens décents, juste des gens qui peuvent s'enliser dans les rapports de bogues et autres et il est donc difficile de ne pas être vraiment laconique parfois.)

@isiahmeadows Merci pour la réponse. Je n'ai pas déposé de nouveau problème car je cherchais d'abord les problèmes en double, ce qui est la bonne chose à faire. J'essayais d'éviter d'enliser les gens parce que je ne savais pas si c'était le bon problème ou même comment catégoriser ce dont je parlais.

ÉDITÉ: Vérifiez la solution @aigoncharov ci -

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

Je ne sais pas si cela peut être amélioré davantage.

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)

Sans commentaire

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

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

Je ne sais pas si cela peut être amélioré davantage.

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)

Sans commentaire

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

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

J'adore cette idée !

Une autre astuce qui pourrait faire le travail consiste à vérifier l'assignabilité dans les deux sens.

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

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

Une nuance ici est de savoir si Exact<{ prop1: 'a' }> doit être attribuable à Exact<{ prop1: string }> . Dans mes cas d'utilisation, il devrait.

@jeremybparagon votre cas est couvert. Voici d'autres cas.

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

Je pense que quiconque utilise cette astuce (et je ne dis pas qu'il n'y a pas d'utilisations valables pour cela) devrait être parfaitement conscient qu'il est très très facile d'obtenir plus d'accessoires dans le type "exact". Puisque InexactType est assignable à Exact<T, InexactType> si vous avez quelque chose comme ça, vous sortez de l'exactitude sans vous en rendre compte :

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

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

Lien de l'aire de jeux

C'est la raison (au moins l'un d'entre eux) pour laquelle TS n'a pas de types exacts, car cela nécessiterait un fork complet des types d'objets en types exacts ou non exacts où un type inexact n'est jamais assignable à un type exact, même si à leur valeur nominale, ils sont compatibles. Le type inexact peut toujours contenir plus de propriétés. (Au moins, c'était l'une des raisons pour lesquelles

Si asExact était un moyen syntaxique de marquer un objet aussi exact, voici à quoi pourrait ressembler une telle solution :

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);

Lien de l'aire de jeux

@dragomirtitian c'est pourquoi j'ai proposé la solution un peu plus tôt https://github.com/microsoft/TypeScript/issues/12936#issuecomment -524631270 qui n'en souffre pas.

@dragomirtitian c'est une question de comment vous tapez vos fonctions.
Si vous le faites un peu différemment, cela fonctionne.

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 votre cas est couvert.

@iamandrewluca Je pense que les solutions ici et ici diffèrent sur la façon dont elles traitent mon exemple .

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...

Lien de l'aire de jeux

@aigoncharov Le problème est que vous devez en être conscient, donc on pourrait facilement ne pas le faire et test1 pourrait toujours être appelé avec des propriétés supplémentaires. OMI, toute solution qui peut si facilement permettre une affectation accidentelle inexacte a déjà échoué, car l'essentiel est de faire respecter l'exactitude dans le système de types.

@toriningen ouais votre solution semble meilleure, je faisais juste référence à la dernière solution publiée. Votre solution a pour avantage le fait que vous n'avez pas besoin du paramètre de type de fonction supplémentaire, mais cela ne semble pas bien fonctionner pour les propriétés facultatives :

// (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 ? 

Lien de l'aire de jeux

@jeremybparagon, je ne suis pas sûr que la solution de T extends S et S extends T souffrira du simple fait 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 

Lien de l'aire de jeux

Je pense que @iamandrewluca d'utiliser Exclude<keyof T, keyof Shape> extends never est bon, mon type est assez similaire (j'ai modifié ma réponse originale pour ajouter le &R pour assurer T extends R sans aucune vérification supplémentaire).

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

Je ne mettrais pas ma réputation en jeu que ma solution n'a pas de trous cependant, je n'ai pas cherché si fort pour eux mais j'accueille de telles découvertes 😊

nous devrions avoir un drapeau où cela est activé globalement. De cette façon, qui veut perdre du type peut continuer à faire de même. Beaucoup trop de bugs causés par ce problème. Maintenant, j'essaie d'éviter l'opérateur de propagation et d'utiliser pickKeysFromObject(shipDataRequest, ['a', 'b','c'])

Voici un cas d'utilisation pour les types exacts sur lesquels je suis récemment tombé :

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]];
    });
;

Parce que les types sont inexacts par défaut, Object.keys doit renvoyer un string[] (voir https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208), mais dans ce cas , si ImmutableMap était exact, il n'y a aucune raison qu'il ne puisse pas renvoyer PossibleKeys[] .

@dallonf notez que cet exemple nécessite des fonctionnalités supplémentaires en plus des types exacts -- Object.keys n'est qu'une fonction et il faudrait un mécanisme pour décrire une fonction qui renvoie keyof T pour les types exacts et string pour les autres types. Avoir simplement la possibilité de déclarer un type exact ne serait pas suffisant.

@RyanCavanaugh Je pense que c'était l'implication, les types exacts + la capacité de les détecter.

Cas d'utilisation pour les typages de réaction :

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

Il est tentant de passer un composant normal à forwardRef c'est pourquoi React émet des avertissements d'exécution s'il détecte propTypes ou defaultProps sur l'argument render . Nous aimerions exprimer cela au niveau du type mais devons revenir à 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>

Le message d'erreur avec never n'est pas utile ("{} n'est pas attribuable à undefined").

Quelqu'un peut-il m'aider sur la façon dont la solution de

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

On ne sait pas comment je pourrais créer une fonction dispatch() typée qui n'accepte que la forme exacte d'un événement.

(MISE À JOUR : j'ai compris : https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)

Cas d'utilisation:

L'absence Exact<> support

Ainsi, lorsque nous obtenons des données du formulaire, Typescript ne peut pas valider les propriétés excédentaires (supplémentaires). Et nous obtiendrons une erreur à l'exécution.

L'exemple suivant illustre la sécurité imaginaire

  • dans le premier cas suivi de tous les paramètres d'entrée
  • mais dans la vraie vie (comme dans le deuxième cas où nous avons obtenu des données du formulaire et les avons enregistrées dans une variable)

Essayez dans l'aire de jeux

Screen Shot 2020-03-05 at 13 04 38

Selon l'article https://fettblog.eu/typescript-match-the-exact-object-shape/ et les solutions similaires fournies ci-dessus, nous pouvons utiliser la solution laide suivante :

Screen Shot 2020-03-05 at 12 26 57

Pourquoi cette solution savePerson<T>(person: ValidateShape<T, Person>) est laide ?

Supposons que vous ayez un type d'entrée profondément imbriqué, par exemple :

// 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>)

Je ne peux pas imaginer quels spaghettis nous devrions écrire pour obtenir le même comportement avec la solution actuellement disponible :

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

Donc, pour l'instant, nous avons de gros trous dans l'analyse statique dans notre code, qui fonctionne avec des données d'entrée imbriquées complexes.

Le cas décrit dans la première image, où TS ne valide pas les propriétés en excès car la "fraîcheur" est perdue, a également été un peu douloureux pour nous.

L'écriture

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

se sent souvent beaucoup moins lisible que

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

Annoter explicitement const options: DoSomethingOptions = { aide, mais c'est un peu lourd et difficile à repérer et à appliquer dans les revues de code.

C'est un peu une idée hors sujet et ne résoudrait pas la plupart des cas d'utilisation pour l'exactitude décrits ici, mais serait-il possible de garder un objet littéral frais lorsqu'il n'est utilisé qu'une seule fois dans la portée englobante ?

@RyanCavanaugh merci d'avoir expliqué l'EPC... la différence entre l'EPC et les types exacts est-elle discutée plus en détail quelque part ? Maintenant, je pense que je devrais mieux comprendre pourquoi EPC autorise certains cas que les types exacts ne permettent pas.

Salut @noppa, je pense que ce serait une excellente idée. Je viens de tomber sur cela lorsque j'ai remarqué la différence entre l'affectation directe et l'affectation à une variable en premier - j'ai même posé une question sur SO qui m'a amené ici. Le comportement actuel est surprenant, du moins pour moi...

Je crois que j'ai le même problème que l'exemple des mutations GraphQL (typage imbriqué exact, aucune propriété supplémentaire ne devrait être autorisée). Dans mon cas, je pense saisir les réponses de l'API dans un module commun (partagé entre le frontend et le backend) :

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

Côté serveur, je voudrais m'assurer que la réponse respecte cette signature de type :

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

J'ai essayé des solutions d'ici. Celui qui semble fonctionner est T extends U ? U extends T ? T : never : never , avec une fonction curry qui n'est pas idéale. Le problème majeur avec cela est que vous n'obtenez aucun retour sur les propriétés manquantes ou supplémentaires (nous pourrions peut-être améliorer cela, mais cela devient difficile à faire lorsque nous entrons dans les propriétés imbriquées). D'autres solutions ne fonctionnent pas avec des objets profondément imbriqués.

Bien sûr, l'interface ne plantera généralement pas si j'envoie plus d'informations que ce qui est spécifié, cependant, cela pourrait entraîner une fuite d'informations si l'API envoie plus d'informations qu'elle ne le devrait (et en raison de la nature brumeuse de la lecture des données d'une base de données quels types ne sont pas nécessairement synchronisés avec le code tout le temps, cela peut arriver).

@ fer22f GraphQL n'envoie pas les champs que le client n'a pas demandés... à moins que vous n'utilisiez un type scalaire JSON pour products ou pour les éléments du tableau, il n'y a rien à craindre.

Désolé d'avoir mal lu, je pensais que vous vouliez dire que vous utilisiez GraphQL

Quelqu'un a déjà mentionné GraphQL, mais juste en termes de "collecte de cas d'utilisation" ( @DanielRosenwasser a mentionné il y a plusieurs années dans le fil :-) de "ne pas avoir de cas d'utilisation à la main"), deux cas d'utilisation où j'ai voulu utiliser Exact sont :

  1. Passer des données dans des magasins de données / bases de données / ORM - tous les champs supplémentaires qui sont transmis seront supprimés / non stockés en silence.

  2. Passer des données dans des appels filaires / RPC / REST / GraphQL - encore une fois, tous les champs supplémentaires qui sont passés seront supprimés / non envoyés en silence.

(Eh bien, peut-être pas supprimés en silence, il peut s'agir d'erreurs d'exécution.)

Dans les deux cas, j'aimerais dire au programmeur/moi-même (via une erreur de compilation) "... vous ne devriez vraiment pas me donner cette propriété supplémentaire, b/c si vous vous attendez à ce qu'elle soit "stockée" ou " envoyé', ce ne sera pas ».

Ceci est particulièrement nécessaire dans les API de style "mise à jour partielle", c'est-à-dire les types faibles :

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

Passe la vérification de type faible car au moins un paramètre correspond, firstName , donc ce n'est pas 100% disjoint, cependant il y a toujours une faute de frappe "évidente" de lsatNmeTypo qui n'est pas détectée.

Certes, EPC fonctionne si je fais :

await saveDataToDbOrWireCall({ firstName, lastNmeTypo });

Mais devoir déstructurer + retaper chaque champ est assez fastidieux.

Des solutions comme le Exactify @jcalz fonctionnent sur la propriété de premier niveau, mais le cas récursif (c'est- children dire que Exact<Foo<Bar<T>> .

Ce serait formidable d'avoir cela intégré, et je voulais juste noter ces cas d'utilisation explicites (essentiellement des appels de câble avec des types partiels/faibles), si cela aide à la hiérarchisation/la feuille de route.

(FWIW https://github.com/stephenh/joist-ts/pull/35/files a ma tentative actuelle à un Exact profond et aussi un Exact.test.ts qui passe des cas triviaux, mais le PR lui-même a des erreurs de compilation sur les usages les plus ésotériques.Avertissement Je ne m'attends pas vraiment à ce que quiconque se penche sur ce PR spécifique, mais je le fournis simplement en tant que "voici où Exact serait utile" + "AFAICT c'est difficile à faire dans le point de données "user-land".)

Hey,

Vous vous demandiez quelles sont les pensées de l'équipe TS concernant les enregistrements de types exacts et la proposition de tuples ici ? https://github.com/tc39/proposal-record-tuple

Est-il judicieux d'introduire des types exacts pour ces nouvelles primitives ?

@slorber Pas TS, mais c'est orthogonal. Cette proposition concerne l'immuabilité, et les préoccupations sont presque identiques entre cela et des bibliothèques comme Immutable.js.

J'ai itéré sur la version récursive de @stephenh . J'ai eu du mal à gérer correctement les cas indéfinis avec récursivité, je suis ouvert à une solution plus propre. Cela ne fonctionne probablement pas sur certains cas extrêmes avec des tableaux ou une structure de données complexe.

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
};

terrain de jeux

Exact nous serait très utile lors du retour des réponses de l'API. Actuellement, c'est ce que nous résolvons à:

const response = { companies };

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

Ici, le type Exact est ce que @ArnaudBarre a fourni ci-dessus.

Merci @ArnaudBarre de m'avoir débloqué et de m'avoir
Riffage sur votre solution:

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 solution précédente a "réussi" pour f6, f8 et f9.
Cette solution renvoie également des résultats « plus propres » ; lorsqu'il correspond, vous récupérez le type « attendu ».
Comme pour le commentaire de @ArnaudBarre ... pas sûr que tous les cas limites soient traités, alors ymmv ...

@heystewart Votre Exact ne donne pas un résultat symétrique :

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

Edit: la version de @ArnaudBarre a également le même problème

@papb Oui effectivement, ma saisie ne fonctionne pas, le point d'entrée est un tableau. J'en avais besoin pour notre API graphQL où variables est toujours un objet.

Pour le résoudre, vous devez isoler ExactObject et ExactArray et avoir un point d'entrée qui va dans l'un ou l'autre.

Alors, quelle est la meilleure façon de s'assurer que cet objet a des propriétés exactes, ni moins, ni plus ?

@captain-yossarian a convaincu l'équipe TypeScript de l'implémenter. Aucune solution présentée ici ne fonctionne pour tous les cas attendus, et presque tous manquent de clarté.

@toriningen ne peut pas imaginer combien de problèmes seront fermés si l'équipe TS implémente cette fonctionnalité

@RyanCavanaugh
À l'heure actuelle, j'ai un cas d'utilisation qui m'a amené ici, et il se retrouve directement dans votre sujet « Miscellany ». Je veux une fonction qui :

  1. prend un paramètre qui implémente une interface avec des paramètres facultatifs
  2. renvoie un objet tapé à l'interface réelle plus étroite du paramètre donné, de sorte que

Ces objectifs immédiats servent ces fins :

  1. Je reçois un excès de propriété en vérifiant l'entrée
  2. J'obtiens la complétion automatique et la sécurité du type de propriété pour la sortie

Exemple

J'ai réduit mon cas à ceci :

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

Cette configuration fonctionne et atteint tous les objectifs souhaités, donc du point de vue de la faisabilité pure et dans la mesure où cela va, je vais bien. Cependant, EPC est obtenu via le paramètre tapant (X extends Y ? Y : X) . Je suis tombé dessus par hasard et j'ai été un peu surpris que cela fonctionne.

Proposition

Et c'est pourquoi j'aimerais avoir un mot-clé implements qui puisse être utilisé à la place de extends afin de marquer l'intention que le type ici n'est pas censé avoir de propriétés excédentaires. Ainsi:

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

Cela me semble beaucoup plus clair que ma solution de contournement actuelle. En plus d'être plus concis, il localise l'ensemble de la contrainte avec la déclaration des génériques par opposition à ma répartition actuelle entre les génériques et les paramètres.

Cela peut également permettre d'autres cas d'utilisation qui sont actuellement impossibles ou peu pratiques, mais ce n'est actuellement qu'une intuition.

Détection de type faible comme alternative

Notamment, la détection de type faible selon # 3842 devrait tout aussi bien résoudre ce problème, et pourrait être favorable car elle ne nécessite pas de syntaxe supplémentaire, si cela fonctionnait en relation avec extends , selon mon cas d'utilisation.

Concernant Exact<Type> etc.

Enfin, implements , comme je l'envisage, devrait être assez simple en ce qui concerne votre argument à propos de function f<T extends Exact<{ n: number }>(p: T) car il n'essaie pas de résoudre le cas plus général de Exact<Type> .

Généralement, Exact<Type> semble être assez peu utile à côté de l'EPC, et je ne peux pas imaginer un cas valide généralement utile qui ne tombe pas en dehors de ces groupes :

  • appels de fonction : ceux-ci peuvent être facilement gérés maintenant selon mon exemple, et bénéficieraient de implements
  • affectations : utilisez simplement des littéraux, donc EPC s'applique
  • des données extérieures à votre domaine de contrôle : la vérification de type ne peut pas vous protéger contre cela, vous devez gérer cela au moment de l'exécution, auquel cas vous revenez à des casts sécurisés

Évidemment, il y aura des cas où vous ne pourrez pas affecter de littéraux, mais ceux-ci devraient également être d'un ensemble fini :

  • si on vous donne les données d'affectation dans une fonction, gérez la vérification de type dans la signature d'appel
  • si vous fusionnez plusieurs objets, conformément à l'OP, affirmez correctement le type de chaque objet source et vous pouvez lancer en toute sécurité as DesiredType

Résumé : implements serait bien mais sinon on est bien

En résumé, je suis convaincu qu'avec implements et la correction de l'EPC (si et quand des problèmes surviennent), les types exacts devraient vraiment être traités.

Question à toutes les parties intéressées : est-ce que quelque chose est réellement ouvert ici ?

Après avoir parcouru les cas d'utilisation ici, je pense que presque toutes les reproductions sont maintenant correctement gérées, et le reste peut fonctionner avec mon petit exemple ci-dessus. Cela soulève la question : est-ce que quelqu'un a encore des problèmes à ce sujet aujourd'hui avec le TS à jour ?

J'ai une idée immature à propos des annotations de type. Faire correspondre un objet est divisé en membres peut être exactement égal, ni plus ni moins, plus ou moins, ni plus mais moins, plus mais pas moins. Pour chacun des cas ci-dessus, il devrait y avoir une expression.

exactement égal, c'est-à-dire ni plus ni moins :

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

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

plus mais pas moins :

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})

pas plus mais moins :

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})

plus ou moins:

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

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

conclusion:

Avec une ligne verticale indique qu'il n'y en a pas moins, sans une ligne verticale signifie qu'il peut y en avoir moins. Avec un signe d'ellipse signifie qu'il peut y en avoir plus, sans un signe d'ellipse signifie qu'il ne peut plus y en avoir. La correspondance des tableaux est la même idée.

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 en utilisant votre exemple, cela compile :

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

Cependant, avec des types exacts, cela ne devrait pas. C'est-à-dire que la demande ici est de ne pas dépendre d'EPC.

@xp44mm "plus mais pas moins" est déjà le comportement et "plus ou moins" est le comportement si vous marquez toutes les propriétés facultatives

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 même, si nous avions des types exacts, type exact + toutes les propriétés facultatives seraient essentiellement "pas plus mais moins" .

Un autre exemple à cette question. Une bonne démonstration pour cette proposition je pense. Dans ce cas, j'utilise rxjs pour travailler avec Subject mais je veux retourner un Observable ("verrouillé") (qui n'a pas next méthode error , etc. pour manipuler le valeur.)

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();
}

Je veux toujours retourner seulement le type exact Observable et non Subject qui l'étend.

Proposition:

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

Mais je suis sûr que vous avez de meilleures idées. Surtout parce que cela n'affecte pas seulement les valeurs de retour, n'est-ce pas ? Bref, juste une démo de pseudo code. Je pense que ce serait une fonctionnalité intéressante pour éviter les erreurs et les manques. Comme dans le cas décrit ci-dessus. Une autre solution pourrait consister à ajouter un nouveau type d'utilitaire .
Ou j'ai raté quelque chose ? Cela fonctionne-t-il déjà ? J'utilise TypeScript 4.

Cette page vous a été utile?
0 / 5 - 0 notes