React+TypeScript复现渐变下划线效果及类型问题排查
问题描述
我想用React和TypeScript复现下划线效果,目前代码里的TypeScript类型识别有问题——已经在Props接口中声明max为number类型,但TypeScript仍将max标记为any类型,不清楚原因。相关TSX代码如下:
import React from 'react'; import Typography from '@mui/material/Typography'; import { Box } from '@mui/material'; interface Props { max: number; } const styles = { body: { width: "80%", margin: "10vw auto", }, heading: { fontFamily: "Playfair Display, serif", fontSize: "10vw", }, "subheading": { fontFamily: "Open Sans, sans-serif", fontSize: "1em", lineHeight: "1.5", }, "@media screen and (min-width: 40em)": { body: { width: "50%", }, heading:{ fontSize: "6vw", }, subheading: { fontSize: "1.75vw", } }, "underline--magical": { backgroundImage: "linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)", backgroundRepeat: "no-repeat", backgroundSize: "100% 0.2em", backgroundPosition: "0 88%", transition: "backgroundSize 0.25s ease-in", "&:hover": { backgroundSize: "100% 88%", }, }, }; function Effect(props: Props) { // VARIABLES const magicalUnderlines = Array.from(document.querySelectorAll('.underline--magical')); const gradientAPI = 'https://gist.githubusercontent.com/wking-io/3e116c0e5675c8bcad8b5a6dc6ca5344/raw/4e783ce3ad0bcd98811c6531e40256b8feeb8fc8/gradient.json'; // HELPER FUNCTIONS // 1. Get random number in range. Used to get random index from array. const randNumInRange = max => Math.floor(Math.random() * (max - 1)); // 2. Merge two separate array values at the same index to // be the same value in new array. const mergeArrays = (arrOne, arrTwo) => arrOne .map((item, i) => `${item} ${arrTwo[i]}`) .join(', '); // 3. Curried function to add a background to array of elms const addBackground = (elms) => (color) => { elms.forEach(el => { el.style.backgroundImage = color; }); } // 4. Function to get data from API const getData = async(url): Promise<XMLHttpRequest> => { const response = await fetch(url); const data = await response.json(); return data.data; } // 5. Partial Application of addBackground to always apply // background to the magicalUnderlines constant const addBackgroundToUnderlines = addBackground(magicalUnderlines); // GRADIENT FUNCTIONS // 1. Build CSS formatted linear-gradient from API data const buildGradient = (obj) => `linear-gradient(${obj.direction}, ${mergeArrays(obj.colors, obj.positions)})`; // 2. Get single gradient from data pulled in array and // apply single gradient to a callback function const applyGradient = async(url, callback): Promise<XMLHttpRequest> => { const data = await getData(url); const gradient = buildGradient(data[randNumInRange(data.length)]); callback(gradient); } // RESULT applyGradient(gradientAPI, addBackgroundToUnderlines); return ( <Box> <Typography sx={styles.heading}> Look At This <span style={styles['underline--magical']}>Pretty</span> Underline </Typography> <Typography sx={styles.subheading}> Wow this one is super incredibly cool, and this{' '} <span style={styles['underline--magical']}>one is on Multiple Lines!</span> I wish I had found this like thirty projects ago when I was representing the lollipop guild. </Typography> </Box> ); } export { Effect };
解决方案
1. 修复max的any类型问题
你声明的Props接口里的max根本没被使用!当前代码里randNumInRange函数的参数max缺少类型注解,这才是TS将其识别为any的原因。给该函数添加类型:
const randNumInRange = (max: number) => Math.floor(Math.random() * (max - 1));
2. 补全所有函数的类型注解
mergeArrays参数类型:
const mergeArrays = (arrOne: string[], arrTwo: string[]) => arrOne .map((item, i) => `${item} ${arrTwo[i]}`) .join(', ');
addBackground参数与返回值类型:
const addBackground = (elms: HTMLElement[]) => (color: string) => { elms.forEach(el => { el.style.backgroundImage = color; }); }
getData返回值类型修正:原代码写的Promise<XMLHttpRequest>错误,需先定义API返回数据的结构:
// 定义API返回的数据类型 interface GradientData { direction: string; colors: string[]; positions: string[]; } interface ApiResponse { data: GradientData[]; } const getData = async(url: string): Promise<GradientData[]> => { const response = await fetch(url); const data: ApiResponse = await response.json(); return data.data; }
buildGradient参数类型:
const buildGradient = (obj: GradientData) => `linear-gradient(${obj.direction}, ${mergeArrays(obj.colors, obj.positions)})`;
applyGradient返回值类型修正:该函数无返回值,应改为Promise<void>:
const applyGradient = async(url: string, callback: (color: string) => void): Promise<void> => { const data = await getData(url); const gradient = buildGradient(data[randNumInRange(data.length)]); callback(gradient); }
3. 修复React组件的DOM操作时机问题
直接在组件函数内调用document.querySelectorAll会导致DOM未挂载时找不到元素,需将DOM操作和API调用移至useEffect钩子中:
import React, { useEffect } from 'react'; import Typography from '@mui/material/Typography'; import { Box } from '@mui/material'; interface Props { max?: number; // 若暂时不用,可设为可选属性 } const styles = { heading: { fontFamily: "Playfair Display, serif", fontSize: "10vw", }, subheading: { fontFamily: "Open Sans, sans-serif", fontSize: "1em", lineHeight: "1.5", }, "@media screen and (min-width: 40em)": { heading:{ fontSize: "6vw", }, subheading: { fontSize: "1.75vw", } }, }; // 单独定义下划线样式,通过类名引用 const underlineStyles = { backgroundImage: "linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)", backgroundRepeat: "no-repeat", backgroundSize: "100% 0.2em", backgroundPosition: "0 88%", transition: "backgroundSize 0.25s ease-in", "&:hover": { backgroundSize: "100% 88%", }, }; function Effect(props: Props) { useEffect(() => { // 类型断言确保是HTMLElement数组 const magicalUnderlines = Array.from(document.querySelectorAll('.underline--magical')) as HTMLElement[]; const gradientAPI = 'https://gist.githubusercontent.com/wking-io/3e116c0e5675c8bcad8b5a6dc6ca5344/raw/4e783ce3ad0bcd98811c6531e40256b8feeb8fc8/gradient.json'; const randNumInRange = (max: number) => Math.floor(Math.random() * (max - 1)); const mergeArrays = (arrOne: string[], arrTwo: string[]) => arrOne .map((item, i) => `${item} ${arrTwo[i]}`) .join(', '); const addBackground = (elms: HTMLElement[]) => (color: string) => { elms.forEach(el => { el.style.backgroundImage = color; }); } interface GradientData { direction: string; colors: string[]; positions: string[]; } interface ApiResponse { data: GradientData[]; } const getData = async(url: string): Promise<GradientData[]> => { const response = await fetch(url); const data: ApiResponse = await response.json(); return data.data; } const addBackgroundToUnderlines = addBackground(magicalUnderlines); const buildGradient = (obj: GradientData) => `linear-gradient(${obj.direction}, ${mergeArrays(obj.colors, obj.positions)})`; const applyGradient = async(url: string, callback: (color: string) => void): Promise<void> => { const data = await getData(url); const gradient = buildGradient(data[randNumInRange(data.length)]); callback(gradient); } applyGradient(gradientAPI, addBackgroundToUnderlines); }, []); // 空依赖数组确保只执行一次 return ( <Box> <Typography sx={styles.heading}> Look At This <span sx={underlineStyles} className="underline--magical">Pretty</span> Underline </Typography> <Typography sx={styles.subheading}> Wow this one is super incredibly cool, and this{' '} <span sx={underlineStyles} className="underline--magical">one is on Multiple Lines!</span> I wish I had found this like thirty projects ago when I was representing the lollipop guild. </Typography> </Box> ); } export { Effect };
注意:将原style={styles['underline--magical']}改为sx={underlineStyles} className="underline--magical",确保DOM元素带有类名,让querySelectorAll能正常找到目标元素。
4. 处理未使用的Props属性
你声明的Props接口里的max未在组件中使用,可选择:
- 将其设为可选属性(如
max?: number) - 删除该属性
- 在组件中使用它(比如用来限制随机数的最大值)
内容的提问来源于stack exchange,提问作者user18943198
相关产品推荐
相关产品推荐

