Obtenez un meilleur aperçu de la façon dont ces outils fonctionnent sous le capot
Apprendre TypeScript est souvent un voyage de redécouverte. Votre première impression peut être assez trompeuse : n’est-ce pas simplement une façon d’annoter JavaScript pour que le compilateur m’aide à trouver des bogues potentiels ?
Bien que cette affirmation soit généralement vraie, au fur et à mesure que vous avancez, vous découvrirez que le pouvoir le plus incroyable du langage réside dans la composition, la déduction et la manipulation de types.
Cet article résumera plusieurs conseils qui vous aideront à utiliser le langage à son plein potentiel.
Le type est un concept courant pour les programmeurs, mais il est étonnamment difficile de le définir succinctement. Je trouve utile d’utiliser Set comme modèle conceptuel à la place.
Par exemple, les nouveaux apprenants trouvent la manière dont TypeScript compose les types contre-intuitive. Voici un exemple très simple :
type Measure = { radius: number };
type Style = { color: string };// typed { radius: number; color: string }
type Circle = Measure & Style;
Si vous interprétez l’opérateur &
dans le sens logique AND
vous pouvez vous attendre Circle
être un type fictif car il s’agit d’une conjonction de deux types sans champs qui se chevauchent. Ce n’est pas ainsi que fonctionne TypeScript. Au lieu de cela, penser en Set est beaucoup plus facile pour déduire le comportement correct. Voici comment procéder :
- Chaque type est un ensemble de valeurs.
- Certains Sets sont infinis : string, object ; certains finis : booléen, indéfini, …
unknown
est l’ensemble universel (y compris toutes les valeurs), tandis quenever
est un ensemble vide (sans valeur).- Taper
Measure
est un ensemble pour tous les objets qui contiennent un champ numérique appeléradius
. De mêmeStyle
. - Le
&
L’opérateur crée une intersection :Measure & Style
désigne un ensemble d’objets contenant à la foisradius
etcolor
champs, effectivement un ensemble plus petit, mais avec des champs plus couramment disponibles. - De même, le
|
L’opérateur crée une Union : un ensemble plus grand mais potentiellement avec moins de champs couramment disponibles (si deux types d’objets sont composés).
Set aide également à comprendre l’assignabilité : une affectation n’est autorisée que si le type de la valeur est un sous-ensemble du type de la destination. Voici à quoi cela ressemble :
type ShapeKind = 'rect' | 'circle';
let foo: string = getSomeString();
let shape: ShapeKind = 'rect';// disallowed because string is not subset of ShapeKind
shape = foo;
// allowed because ShapeKind is subset of string
foo = shape;
L’article suivant fournit une excellente introduction élaborée à la pensée dans Set.
Une fonctionnalité TypeScript extrêmement puissante est la réduction automatique des types basée sur le flux de contrôle. Cela signifie qu’une variable a deux types qui lui sont associés à n’importe quel point spécifique de l’emplacement du code : un type de déclaration et un type restreint.
unction foo(x: string | number) {
if (typeof x === 'string') {
// x's type is narrowed to string, so .length is valid
console.log(x.length);// assignment respects declaration type, not narrowed type
x = 1;
console.log(x.length); // disallowed because x is now number
} else {
...
}
}
Lors de la définition d’un ensemble de types polymorphes comme Shape
il est facile de commencer avec ce qui suit :
type Shape = {
kind: 'circle' | 'rect';
radius?: number;
width?: number;
height?: number;
}function getArea(shape: Shape) {
return shape.kind === 'circle' ?
Math.PI * shape.radius! ** 2
: shape.width! * shape.height!;
}
Les assertions non nulles (lors de l’accès radius
, width
et height
champs) sont nécessaires car il n’y a pas de relation établie entre kind
et d’autres domaines. Au lieu de cela, un syndicat discriminé est une bien meilleure solution. Voici à quoi cela ressemble :
type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;function getArea(shape: Shape) {
return shape.kind === 'circle' ?
Math.PI * shape.radius ** 2
: shape.width * shape.height;
}
Le rétrécissement de type a éliminé le besoin de coercition.
Si vous utilisez TypeScript de la bonne manière, vous devriez rarement vous retrouver à utiliser une assertion de type explicite (comme value as SomeType
); cependant, parfois, vous ressentirez toujours une impulsion, comme celle-ci :
type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;function isCircle(shape: Shape) {
return shape.kind === 'circle';
}
function isRect(shape: Shape) {
return shape.kind === 'rect';
}
const myShapes: Shape[] = getShapes();
// error because TypeScript doesn't know the filtering
// narrows typing
const circles: Circle[] = myShapes.filter(isCircle);
// you may be inclined to add an assertion:
// const circles = myShapes.filter(isCircle) as Circle[];
Une solution plus élégante consiste à changer isCircle
et isRect
pour renvoyer le prédicat de type à la place, afin qu’ils aident TypeScript à affiner davantage les types après le filter
téléphoner à. Voici à quoi cela ressemble :
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle';
}function isRect(shape: Shape): shape is Rect {
return shape.kind === 'rect';
}
...
// now you get Circle[] type inferred correctly
const circles = myShapes.filter(isCircle);
L’inférence de type est l’instinct de TypeScript. La plupart du temps, cela fonctionne silencieusement pour vous. Cependant, vous devrez peut-être intervenir dans des cas subtils d’ambiguïtés. Les types conditionnels distributifs sont l’un de ces cas.
Supposons que nous ayons un ToArray
type d’assistance qui renvoie un type de tableau si le type d’entrée n’en est pas déjà un, comme ceci :
type ToArray<T> = T extends Array<unknown> ? T: T[];
Selon vous, que faut-il déduire pour le type suivant ?
type Foo = ToArray<string|number>;
La réponse est string[] | number[]
. Mais ceci est ambigu. Pourquoi pas (string | number)[]
au lieu?
Par défaut, lorsque TypeScript rencontre un type union (string | number
ici) pour un paramètre générique (T
ici), il distribue dans chaque constituant, et c’est pourquoi vous obtenez string[] | number[]
. Ce comportement peut être modifié en utilisant une syntaxe spéciale et un habillage T
dans une paire de []
aimer:
type ToArray<T> = [T] extends [Array<unknown>] ? T : T[];
type Foo = ToArray<string | number>;
Maintenant Foo
est déduit comme type (string | number)[]
.
Lorsque vous changez de casse sur une énumération, c’est une bonne habitude de se tromper activement pour les cas qui ne sont pas attendus au lieu de les ignorer silencieusement comme vous le faites dans d’autres langages de programmation. Voici comment procéder :
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rect':
return shape.width * shape.height;
default:
throw new Error('Unknown shape kind');
}
}
Avec TypeScript, vous pouvez laisser la vérification de type statique trouver l’erreur plus tôt pour vous en utilisant le never
taper:
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rect':
return shape.width * shape.height;
default:
// you'll get a type-checking error below
// if any shape.kind is not handled above
const _exhaustiveCheck: never = shape;
throw new Error('Unknown shape kind');
}
}
Avec cela, il est impossible d’oublier de mettre à jour le getArea
fonction lors de l’ajout d’un nouveau type de forme.
La raison d’être de la technique est que la never
le type ne peut pas être affecté avec quoi que ce soit d’autre que never
. Si tous les candidats de shape.kind
sont épuisés par les instructions case, le seul type possible atteignant default
n’est jamais; cependant, si un candidat n’est pas couvert, il fuira vers le default
branche et entraîner une affectation non valide.
En TypeScript, type
et interface
sont des constructions très similaires lorsqu’elles sont utilisées pour taper des objets. Bien que peut-être controversé, ma recommandation est d’utiliser systématiquement type
dans la plupart des cas et n’utiliser que interface
lorsque l’une des conditions suivantes est vraie :
- Vous souhaitez profiter du «fusionner » caractéristique de
interface
. - Vous avez un code de style OO impliquant des hiérarchies de classe/interface.
Sinon, toujours utiliser le plus polyvalent type
construire des résultats dans un code plus cohérent.
Les types d’objets sont le moyen courant de saisir des données structurées, mais vous pouvez parfois souhaiter une représentation plus concise et utiliser à la place des tableaux simples. Par exemple, notre Circle
peut être défini comme ceci :
type Circle = (string | number)[];
const circle: Circle = ['circle', 1.0]; // [kind, radius]
Mais cette frappe est inutilement lâche, et vous pouvez facilement faire une erreur en créant quelque chose comme ['circle', '1.0']
. Nous pouvons le rendre plus strict en utilisant Tuple à la place. Voici comment procéder :
type Circle = [string, number];// you'll get an error below
const circle: Circle = ['circle', '1.0'];
Un bon exemple d’utilisation de Tuple est celui de React useState
.
const [name, setName] = useState('');
Il est à la fois compact et sécurisé.
TypeScript utilise un comportement par défaut sensible lors de l’inférence de type, qui vise à faciliter l’écriture de code pour les cas courants (les types n’ont donc pas besoin d’être explicitement annotés). Il existe plusieurs façons de modifier son comportement.
- Utilisation
const
se limiter au type le plus spécifique
let foo = { name: 'foo' }; // typed: { name: string }
let Bar = { name: 'bar' } as const; // typed: { name: 'bar' }let a = [1, 2]; // typed: number[]
let b = [1, 2] as const; // typed: [1, 2]
// typed { kind: 'circle; radius: number }
let circle = { kind: 'circle' as const, radius: 1.0 };
// the following won't work if circle wasn't initialized
// with the const keyword
let shape: { kind: 'circle' | 'rect' } = circle;
- Utilisation
satisfies
pour vérifier le typage sans affecter le type déduit
Considérez l’exemple suivant :
type NamedCircle = {
radius: number;
name?: string;
};const circle: NamedCircle = { radius: 1.0, name: 'yeah' };
// error because circle.name can be undefined
console.log(circle.name.length);
Nous avons une erreur parce que, selon circle
type de déclaration NamedCircle
, name
Le champ peut être indéfini, même si l’initialiseur de variable a fourni une valeur de chaîne. Bien sûr, nous pouvons laisser tomber le : NamedCircle
annotation de type, mais nous perdrons la vérification de type pour la validité du circle
objet. Tout un dilemme.
Heureusement, TypeScript 4.9 a introduit un nouveau satisfies
mot-clé, qui vous permet de vérifier le type sans modifier le type déduit. Voici comment procéder :
type NamedCircle = {
radius: number;
name?: string;
};// error because radius violates NamedCircle
const wrongCircle = { radius: '1.0', name: 'ha' }
satisfies NamedCircle;
const circle = { radius: 1.0, name: 'yeah' }
satisfies NamedCircle;
// circle.name can't be undefined now
console.log(circle.name.length);
La version modifiée bénéficie des deux avantages : le littéral d’objet est garanti conforme à NamedCircle
type et le type inféré a un non-nullable name
domaine.
Lors de la conception de fonctions et de types utilitaires, vous devrez souvent utiliser un type extrait du paramètre de type donné. Le infer
mot-clé est utile dans cette situation. Il vous aide à déduire un nouveau paramètre de type à la volée. Voici deux exemples simples :
// gets the unwrapped type out of a Promise;
// idempotent if T is not Promise
type ResolvedPromise<T> = T extends Promise<infer U> ? U : T;
type t = ResolvedPromise<Promise<string>>; // t: string// gets the flattened type of array T;
// idempotent if T is not array
type Flatten<T> = T extends Array<infer E> ? Flatten<E> : T;
type e = Flatten<number[][]>; // e: number
Comment infer
le mot-clé fonctionne dans T extends Promise<infer U>
peut être compris comme supposant T
est compatible avec certains génériques instanciés Promise
type, improviser un paramètre de type U
pour le faire fonctionner. Donc si T
est instancié comme Promise<string>
la résolution de U
sera string
.
TypeScript fournit de puissantes syntaxes de manipulation de type et un ensemble d’utilitaires très utiles pour vous aider à réduire au minimum la duplication de code. Voici quelques exemples ad hoc :
- Au lieu de dupliquer les déclarations de champs,
type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = { age: number: gender: string; };
type Geo = { country: string; city: string; };
Utilisez le Pick
utilitaire pour extraire de nouveaux types :
type User = {
age: number;
gender: string;
country: string;
city: string
};
type Demographic = Pick<User, 'age'|'gender'>;
type Geo = Pick<User, 'country'|'city'>;
- Au lieu de dupliquer le type de retour de la fonction,
function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}function transformCircle(circle: { kind: 'circle'; radius: number }) {
...
}
transformCircle(createCircle());
utilisation ReturnType<T>
pour l’extraire :
function createCircle() {
return {
kind: 'circle' as const,
radius: 1.0
}
}function transformCircle(circle: ReturnType<typeof createCircle>) {
...
}
transformCircle(createCircle());
- Au lieu de synchroniser des formes de deux types (
typeof
configuration etFactory
ici) en parallèle,
type ContentTypes = 'news' | 'blog' | 'video';// config for indicating what content types are enabled
const config = { news: true, blog: true, video: false }
satisfies Record<ContentTypes, boolean>;
// factory for creating contents
type Factory = {
createNews: () => Content;
createBlog: () => Content;
};
utilisation Type mappé et Type littéral du modèle pour déduire automatiquement le type de fabrique approprié en fonction de la forme de la configuration :
type ContentTypes = 'news' | 'blog' | 'video';// generic factory type with a inferred list of methods
// based on the shape of the given Config
type ContentFactory<Config extends Record<ContentTypes, boolean>> = {
[k in string & keyof Config as Config[k] extends true
? `create${Capitalize<k>}`
: never]: () => Content;
};
// config for indicating what content types are enabled
const config = { news: true, blog: true, video: false }
satisfies Record<ContentTypes, boolean>;
type Factory = ContentFactory<typeof config>;
// Factory: {
// createNews: () => Content;
// createBlog: () => Content;
// }
Utilisez votre imagination et vous trouverez un potentiel infini à explorer.