You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 00:18:22